From 66486541fee34027825757c7f4c50453c930e10b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 16 Jul 2025 21:30:16 +0300 Subject: [PATCH 001/232] feat(client): batch delete column values --- apps/client/src/services/dialog.ts | 8 ++- .../src/translations/en/translation.json | 4 +- .../view_widgets/table_view/bulk_actions.ts | 53 +++++++++++++------ .../view_widgets/table_view/context_menu.ts | 37 ++++++++++--- 4 files changed, 76 insertions(+), 26 deletions(-) diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index 298c7bf8a9..a1e54f5e8c 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -41,8 +41,14 @@ async function info(message: string) { return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); } +/** + * Displays a confirmation dialog with the given message. + * + * @param message the message to display in the dialog. + * @returns A promise that resolves to true if the user confirmed, false otherwise. + */ async function confirm(message: string) { - return new Promise((res) => + return new Promise((res) => appContext.triggerCommand("showConfirmDialog", { message, callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index cfc80a537f..281554d667 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1956,7 +1956,9 @@ "row-insert-child": "Insert child note", "add-column-to-the-left": "Add column to the left", "add-column-to-the-right": "Add column to the right", - "edit-column": "Edit column" + "edit-column": "Edit column", + "delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.", + "delete-column": "Delete column" }, "book_properties_config": { "hide-weekends": "Hide weekends", diff --git a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts index c930b79e4c..48262b5827 100644 --- a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts +++ b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts @@ -6,27 +6,48 @@ import toast from "../../../services/toast"; import ws from "../../../services/ws"; export async function renameColumn(parentNoteId: string, type: "label" | "relation", originalName: string, newName: string) { + if (type === "label") { + return executeBulkAction(parentNoteId, { + name: "renameLabel", + oldLabelName: originalName, + newLabelName: newName + }); + } else { + return executeBulkAction(parentNoteId, { + name: "renameRelation", + oldRelationName: originalName, + newRelationName: newName + }); + } +} + +export async function deleteColumn(parentNoteId: string, type: "label" | "relation", columnName: string) { + if (type === "label") { + return executeBulkAction(parentNoteId, { + name: "deleteLabel", + labelName: columnName + }); + } else { + return executeBulkAction(parentNoteId, { + name: "deleteRelation", + relationName: columnName + }); + } +} + +async function executeBulkAction(parentNoteId: string, action: {}) { const bulkActionNote = await froca.getNote("_bulkAction"); if (!bulkActionNote) { console.warn("Bulk action note not found"); return; } - if (type === "label") { - attributes.setLabel("_bulkAction", "action", JSON.stringify({ - name: "renameLabel", - oldLabelName: originalName, - newLabelName: newName - })); - await server.post("bulk-action/execute", { - noteIds: [ parentNoteId ], - includeDescendants: true - }); + attributes.setLabel("_bulkAction", "action", JSON.stringify(action)); + await server.post("bulk-action/execute", { + noteIds: [ parentNoteId ], + includeDescendants: true + }); - await ws.waitForMaxKnownEntityChangeId(); - toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); - } else { - console.warn("Renaming relation columns is not supported yet"); - return; - } + await ws.waitForMaxKnownEntityChangeId(); + toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); } diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index 951d1ba25b..c6f619cbcb 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -7,10 +7,12 @@ import link_context_menu from "../../../menus/link_context_menu.js"; import type FNote from "../../../entities/fnote.js"; import froca from "../../../services/froca.js"; import type Component from "../../../components/component.js"; +import dialog from "../../../services/dialog.js"; +import { deleteColumn } from "./bulk_actions.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); - tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, tabulator)); + tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator)); // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. if (tabulator.options.dataTree) { @@ -20,7 +22,7 @@ export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { } } -function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: Tabulator) { +function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) { const e = _e as MouseEvent; const { title, field } = column.getDefinition(); @@ -87,6 +89,15 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: referenceColumn: column }) }, + { + title: t("table_view.add-column-to-the-right"), + uiIcon: "bx bx-horizontal-right", + handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { + referenceColumn: column, + direction: "after" + }) + }, + { title: "----" }, { title: t("table_view.edit-column"), uiIcon: "bx bx-edit", @@ -97,12 +108,22 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: }) }, { - title: t("table_view.add-column-to-the-right"), - uiIcon: "bx bx-horizontal-right", - handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { - referenceColumn: column, - direction: "after" - }) + title: t("table_view.delete-column"), + uiIcon: "bx bx-trash", + enabled: !!column.getField() && column.getField() !== "title", + handler: async () => { + if (!await dialog.confirm(t("table_view.delete_column_confirmation"))) { + return; + } + + let [ type, name ] = column.getField()?.split(".", 2); + if (!type || !name) { + return; + } + + type = type.replace("s", ""); + deleteColumn(parentNote.noteId, type as "label" | "relation", name); + } } ], selectMenuItemHandler() {}, From e7f47a0663e256898153c59d81f8ad97807a0ee7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 16 Jul 2025 21:40:01 +0300 Subject: [PATCH 002/232] feat(views/table): delete column definition as well --- apps/client/src/components/app_context.ts | 3 ++ .../view_widgets/table_view/col_editing.ts | 42 +++++++++++++++---- .../view_widgets/table_view/context_menu.ts | 20 ++------- .../widgets/view_widgets/table_view/index.ts | 21 +++------- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 24545fdaa6..73187131c5 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -288,6 +288,9 @@ export type CommandMappings = { referenceColumn?: ColumnComponent; direction?: "before" | "after"; }; + deleteTableColumn: CommandData & { + columnToDelete?: ColumnComponent; + }; buildTouchBar: CommandData & { TouchBar: typeof TouchBar; diff --git a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts index 4fdc8b6ab0..b9c95e35cb 100644 --- a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts +++ b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts @@ -5,7 +5,9 @@ import Component from "../../../components/component"; import { CommandListenerData, EventData } from "../../../components/app_context"; import attributes from "../../../services/attributes"; import FNote from "../../../entities/fnote"; -import { renameColumn } from "./bulk_actions"; +import { deleteColumn, renameColumn } from "./bulk_actions"; +import dialog from "../../../services/dialog"; +import { t } from "../../../services/i18n"; export default class TableColumnEditing extends Component { @@ -79,18 +81,40 @@ export default class TableColumnEditing extends Component { const { name, type, value } = this.newAttribute; this.api.blockRedraw(); + try { + if (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name) { + const oldName = this.existingAttributeToEdit.name.split(":")[1]; + const newName = name.split(":")[1]; + await renameColumn(this.parentNote.noteId, type, oldName, newName); + } - if (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name) { - const oldName = this.existingAttributeToEdit.name.split(":")[1]; - const newName = name.split(":")[1]; - await renameColumn(this.parentNote.noteId, type, oldName, newName); + attributes.setLabel(this.parentNote.noteId, name, value); + if (this.existingAttributeToEdit) { + attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name); + } + } finally { + this.api.restoreRedraw(); + } + } + + async deleteTableColumnCommand({ columnToDelete }: CommandListenerData<"deleteTableColumn">) { + if (!columnToDelete || !await dialog.confirm(t("table_view.delete_column_confirmation"))) { + return; } - attributes.setLabel(this.parentNote.noteId, name, value); - if (this.existingAttributeToEdit) { - attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name); + let [ type, name ] = columnToDelete.getField()?.split(".", 2); + if (!type || !name) { + return; + } + type = type.replace("s", ""); + + this.api.blockRedraw(); + try { + await deleteColumn(this.parentNote.noteId, type as "label" | "relation", name); + attributes.removeOwnedLabelByName(this.parentNote, `${type}:${name}`); + } finally { + this.api.restoreRedraw(); } - this.api.restoreRedraw(); } getNewAttributePosition() { diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index c6f619cbcb..ee57e31805 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -1,4 +1,4 @@ -import { ColumnComponent, MenuSeparator, RowComponent, Tabulator } from "tabulator-tables"; +import { ColumnComponent, RowComponent, Tabulator } from "tabulator-tables"; import contextMenu, { MenuItem } from "../../../menus/context_menu.js"; import { TableData } from "./rows.js"; import branches from "../../../services/branches.js"; @@ -7,8 +7,6 @@ import link_context_menu from "../../../menus/link_context_menu.js"; import type FNote from "../../../entities/fnote.js"; import froca from "../../../services/froca.js"; import type Component from "../../../components/component.js"; -import dialog from "../../../services/dialog.js"; -import { deleteColumn } from "./bulk_actions.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); @@ -111,19 +109,9 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: title: t("table_view.delete-column"), uiIcon: "bx bx-trash", enabled: !!column.getField() && column.getField() !== "title", - handler: async () => { - if (!await dialog.confirm(t("table_view.delete_column_confirmation"))) { - return; - } - - let [ type, name ] = column.getField()?.split(".", 2); - if (!type || !name) { - return; - } - - type = type.replace("s", ""); - deleteColumn(parentNote.noteId, type as "label" | "relation", name); - } + handler: () => getParentComponent(e)?.triggerCommand("deleteTableColumn", { + columnToDelete: column + }) } ], selectMenuItemHandler() {}, diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index c2f5271087..e664e8cd8d 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -221,22 +221,11 @@ export default class TableView extends ViewMode { this.colEditing?.resetNewAttributePosition(); } - addNewRowCommand(e) { - this.rowEditing?.addNewRowCommand(e); - } - - addNewTableColumnCommand(e) { - this.colEditing?.addNewTableColumnCommand(e); - } - - updateAttributeListCommand(e) { - this.colEditing?.updateAttributeListCommand(e); - } - - saveAttributesCommand() { - this.colEditing?.saveAttributesCommand(); - } - + addNewRowCommand(e) { this.rowEditing?.addNewRowCommand(e); } + addNewTableColumnCommand(e) { this.colEditing?.addNewTableColumnCommand(e); } + deleteTableColumnCommand(e) { this.colEditing?.deleteTableColumnCommand(e); } + updateAttributeListCommand(e) { this.colEditing?.updateAttributeListCommand(e); } + saveAttributesCommand() { this.colEditing?.saveAttributesCommand(); } async #manageRowsUpdate() { if (!this.api) { From 275aacfba9a1448db3a4383c6896e17a26cf5f2a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 16 Jul 2025 21:40:14 +0300 Subject: [PATCH 003/232] chore(vscode): add search excludes --- .vscode/settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a27649837..9ee96f4c1b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,12 @@ "typescript.validate.enable": true, "typescript.tsserver.experimental.enableProjectDiagnostics": true, "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "search.exclude": { + "**/node_modules": true, + "docs/**/*.html": true, + "docs/**/*.png": true, + "apps/server/src/assets/doc_notes/**": true, + "apps/edit-docs/demo/**": true + } } \ No newline at end of file From 0f129734aea7a9921c878e45c75715a793a061a7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 11:19:29 +0300 Subject: [PATCH 004/232] fix(link): popup triggering with bare right click --- apps/client/src/services/link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index c03ab536a0..b9141ff0fb 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -316,7 +316,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey; if (notePath) { - if (openInPopup) { + if (isLeftClick && openInPopup) { appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath }); } else if (openInNewWindow) { appContext.triggerCommand("openInWindow", { notePath, viewScope }); From df3b9faf8d34b016f9f0a8999c90e2b05fc4ac5e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 14:05:19 +0300 Subject: [PATCH 005/232] fix(client): tree operations no longer working due to loss of focus --- apps/client/src/widgets/type_widgets/type_widget.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/type_widget.ts b/apps/client/src/widgets/type_widgets/type_widget.ts index 9cd8a64abd..31ecbf6174 100644 --- a/apps/client/src/widgets/type_widgets/type_widget.ts +++ b/apps/client/src/widgets/type_widgets/type_widget.ts @@ -76,8 +76,10 @@ export default abstract class TypeWidget extends NoteContextAwareWidget { return; } - // Restore focus to the editor when switching tabs. - this.focus(); + // Restore focus to the editor when switching tabs, but only if the note tree is not already focused. + if (!document.activeElement?.classList.contains("fancytree-title")) { + this.focus(); + } } /** From 27d515f2893afefb9f84468f35d34f2d83914cba Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 14:34:40 +0300 Subject: [PATCH 006/232] refactor(views/table): use builtin way of disabling branch elements --- apps/client/src/stylesheets/table.css | 4 ---- apps/client/src/widgets/view_widgets/table_view/index.ts | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/client/src/stylesheets/table.css b/apps/client/src/stylesheets/table.css index 81130a7e17..81b93568fd 100644 --- a/apps/client/src/stylesheets/table.css +++ b/apps/client/src/stylesheets/table.css @@ -152,10 +152,6 @@ color: var(--row-text-color); } -.tabulator-data-tree-branch { - visibility: hidden; -} - /* Checkbox cells */ .tabulator .tabulator-cell:has(svg), diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index e664e8cd8d..45464da7bc 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -159,6 +159,7 @@ export default class TableView extends ViewMode { ...opts, dataTree: hasChildren, dataTreeStartExpanded: true, + dataTreeBranchElement: false, dataTreeElementColumn: "title", dataTreeExpandElement: ``, dataTreeCollapseElement: `` From bd840a24217c940f7c4c3d15985b8cbaacdcebc4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 14:40:44 +0300 Subject: [PATCH 007/232] feat(views/table): align items expanders --- apps/client/src/stylesheets/table.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/client/src/stylesheets/table.css b/apps/client/src/stylesheets/table.css index 81b93568fd..175f1e422d 100644 --- a/apps/client/src/stylesheets/table.css +++ b/apps/client/src/stylesheets/table.css @@ -152,6 +152,11 @@ color: var(--row-text-color); } +/* Align items without children/expander to the ones with. */ +.tabulator-cell > div:first-child + span { + padding-left: 21px; +} + /* Checkbox cells */ .tabulator .tabulator-cell:has(svg), From 8b0fdaccf499a266b3ede4498d0b47d98d3207b7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 14:45:38 +0300 Subject: [PATCH 008/232] feat(views/table): improve alignment for first level + increase indentation --- apps/client/src/stylesheets/table.css | 3 ++- apps/client/src/widgets/view_widgets/table_view/index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/stylesheets/table.css b/apps/client/src/stylesheets/table.css index 175f1e422d..39698f345d 100644 --- a/apps/client/src/stylesheets/table.css +++ b/apps/client/src/stylesheets/table.css @@ -153,7 +153,8 @@ } /* Align items without children/expander to the ones with. */ -.tabulator-cell > div:first-child + span { +.tabulator-cell > span:first-child, /* 1st level */ +.tabulator-cell > div:first-child + span { /* sub-level */ padding-left: 21px; } diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index 45464da7bc..aea8c6c259 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -161,6 +161,7 @@ export default class TableView extends ViewMode { dataTreeStartExpanded: true, dataTreeBranchElement: false, dataTreeElementColumn: "title", + dataTreeChildIndent: 20, dataTreeExpandElement: ``, dataTreeCollapseElement: `` } From a25ce42490c67f4084fc97fb4ed082b877a77735 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 15:00:10 +0300 Subject: [PATCH 009/232] feat(views/table): allow hiding number row & title --- .../widgets/view_widgets/table_view/columns.spec.ts | 11 +++++++++++ .../src/widgets/view_widgets/table_view/columns.ts | 6 +++--- .../widgets/view_widgets/table_view/context_menu.ts | 2 -- .../src/widgets/view_widgets/table_view/index.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/columns.spec.ts b/apps/client/src/widgets/view_widgets/table_view/columns.spec.ts index 713c85d0ac..2c46858920 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.spec.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.spec.ts @@ -108,4 +108,15 @@ describe("restoreExistingData", () => { const restored = restoreExistingData(newDefs, oldDefs); expect(restored).toStrictEqual(newDefs); }); + + it("allows hiding the row number column", () => { + const newDefs: ColumnDefinition[] = [ + { title: "#", headerSort: false, hozAlign: "center", resizable: false, frozen: true, rowHandle: false }, + ] + const oldDefs: ColumnDefinition[] = [ + { title: "#", headerSort: false, hozAlign: "center", resizable: false, rowHandle: false, visible: false }, + ]; + const restored = restoreExistingData(newDefs, oldDefs); + expect(restored[0].visible).toStrictEqual(false); + }); }); diff --git a/apps/client/src/widgets/view_widgets/table_view/columns.ts b/apps/client/src/widgets/view_widgets/table_view/columns.ts index 356f32f29c..f75e5499f1 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -41,7 +41,7 @@ const labelTypeMappings: Record> = { } }; -export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[], position?: number) { +export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData: ColumnDefinition[] | undefined, position?: number) { let columnDefs: ColumnDefinition[] = [ { title: "#", @@ -102,10 +102,10 @@ export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: Column .filter(item => (item.field && newItemsByField.has(item.field!)) || item.title === "#") .map(oldItem => { const data = newItemsByField.get(oldItem.field!)!; - if (oldItem.width) { + if (oldItem.width !== undefined) { data.width = oldItem.width; } - if (oldItem.visible) { + if (oldItem.visible !== undefined) { data.visible = oldItem.visible; } return data; diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index ee57e31805..dbac8e0a45 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -69,7 +69,6 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: }, { title: t("table_view.hide-column", { title }), - enabled: !!field, uiIcon: "bx bx-hide", handler: () => column.hide() }, @@ -129,7 +128,6 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: title, checked: column.isVisible(), uiIcon: "bx bx-empty", - enabled: !!field, handler: () => column.toggle() }); } diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index aea8c6c259..9fb165a8df 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -137,7 +137,7 @@ export default class TableView extends ViewMode { const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info); const movableRows = canReorderRows(this.parentNote) && !hasChildren; - const columnDefs = buildColumnDefinitions(info, movableRows); + const columnDefs = buildColumnDefinitions(info, movableRows, this.persistentData.columns); let opts: Options = { layout: "fitDataFill", index: "branchId", From aef824d262f0426eeabfa5c2858094e097c478e4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 15:36:33 +0300 Subject: [PATCH 010/232] feat(views/table): add a context menu for the header outside columns --- .../view_widgets/table_view/context_menu.ts | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index dbac8e0a45..50f72092ca 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -11,6 +11,10 @@ import type Component from "../../../components/component.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator)); + tabulator.on("renderComplete", () => { + const headerRow = tabulator.element.querySelector(".tabulator-header-contents"); + headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(e, tabulator)); + }); // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. if (tabulator.options.dataTree) { @@ -75,7 +79,7 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: { title: t("table_view.show-hide-columns"), uiIcon: "bx bx-empty", - items: buildColumnItems() + items: buildColumnItems(tabulator) }, { title: "----" }, { @@ -118,22 +122,32 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: y: e.pageY }); e.preventDefault(); +} - function buildColumnItems() { - const items: MenuItem[] = []; - for (const column of tabulator.getColumns()) { - const { title, field } = column.getDefinition(); - - items.push({ - title, - checked: column.isVisible(), +/** + * Shows a context menu which has options dedicated to the header area (the part where the columns are, but in the empty space). + * Provides generic options such as toggling columns. + */ +function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { + const e = _e as MouseEvent; + contextMenu.show({ + items: [ + { + title: t("table_view.show-hide-columns"), uiIcon: "bx bx-empty", - handler: () => column.toggle() - }); - } - - return items; - } + items: buildColumnItems(tabulator) + }, + { + title: t("table_view.new-column"), + uiIcon: "bx bx-columns", + handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {}) + }, + ], + selectMenuItemHandler() {}, + x: e.pageX, + y: e.pageY + }); + e.preventDefault(); } export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { @@ -213,3 +227,19 @@ function getParentComponent(e: MouseEvent) { .closest(".component") .prop("component") as Component; } + +function buildColumnItems(tabulator: Tabulator) { + const items: MenuItem[] = []; + for (const column of tabulator.getColumns()) { + const { title } = column.getDefinition(); + + items.push({ + title, + checked: column.isVisible(), + uiIcon: "bx bx-empty", + handler: () => column.toggle() + }); + } + + return items; +} From 876c6e9252195b63da1212d077379c2fd8ca4569 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 19:34:29 +0300 Subject: [PATCH 011/232] feat(views/table): allow limiting depth --- .../src/widgets/view_widgets/table_view/index.ts | 12 ++++++++++-- .../src/widgets/view_widgets/table_view/rows.ts | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index 9fb165a8df..3e2f42e6b3 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -104,6 +104,7 @@ export default class TableView extends ViewMode { private persistentData: StateInfo["tableData"]; private colEditing?: TableColumnEditing; private rowEditing?: TableRowEditing; + private maxDepth: number = -1; constructor(args: ViewModeArgs) { super(args, "table"); @@ -135,7 +136,8 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; - const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info); + this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10); + const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); const movableRows = canReorderRows(this.parentNote) && !hasChildren; const columnDefs = buildColumnDefinitions(info, movableRows, this.persistentData.columns); let opts: Options = { @@ -203,6 +205,12 @@ export default class TableView extends ViewMode { return await this.#manageRowsUpdate(); } + // Refresh max depth + if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "maxNestingDepth" && attributes.isAffecting(attr, this.parentNote))) { + this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10); + return await this.#manageRowsUpdate(); + } + if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? "")) || loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId) || loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!)))) { @@ -235,7 +243,7 @@ export default class TableView extends ViewMode { } const info = getAttributeDefinitionInformation(this.parentNote); - const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info); + const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info, this.maxDepth); // Force a refresh if the data tree needs enabling/disabling. if (this.api.options.dataTree !== hasSubtree) { diff --git a/apps/client/src/widgets/view_widgets/table_view/rows.ts b/apps/client/src/widgets/view_widgets/table_view/rows.ts index 615dd7f9be..3ab5cda9cc 100644 --- a/apps/client/src/widgets/view_widgets/table_view/rows.ts +++ b/apps/client/src/widgets/view_widgets/table_view/rows.ts @@ -12,7 +12,7 @@ export type TableData = { _children?: TableData[]; }; -export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[]) { +export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[], maxDepth = -1, currentDepth = 0) { const definitions: TableData[] = []; let hasSubtree = false; for (const branch of parentNote.getChildBranches()) { @@ -40,8 +40,8 @@ export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDef branchId: branch.branchId, } - if (note.hasChildren()) { - def._children = (await buildRowDefinitions(note, infos)).definitions; + if (note.hasChildren() && (maxDepth < 0 || currentDepth < maxDepth)) { + def._children = (await buildRowDefinitions(note, infos, maxDepth, currentDepth + 1)).definitions; hasSubtree = true; } From a5db5298a03a30534c3a6072d5dd7fc1d7493ea5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 17 Jul 2025 19:44:34 +0300 Subject: [PATCH 012/232] feat(views/table): integrate depth limit into collection properties --- .../widgets/ribbon_widgets/book_properties.ts | 16 ++++++++++++++++ .../ribbon_widgets/book_properties_config.ts | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/ribbon_widgets/book_properties.ts b/apps/client/src/widgets/ribbon_widgets/book_properties.ts index 39c4e92b20..13df6ce47c 100644 --- a/apps/client/src/widgets/ribbon_widgets/book_properties.ts +++ b/apps/client/src/widgets/ribbon_widgets/book_properties.ts @@ -168,6 +168,22 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { }); $container.append($button); break; + case "number": + const $numberInput = $("", { + type: "number", + class: "form-control form-control-sm", + value: note.getLabelValue(property.bindToLabel) || "", + }); + $numberInput.on("change", () => { + const value = $numberInput.val(); + if (value === "") { + attributes.removeOwnedLabelByName(note, property.bindToLabel); + } else { + attributes.setLabel(note.noteId, property.bindToLabel, String(value)); + } + }); + $container.append($("