From 0f1c5058234b635d3ed047324c1f1ca1aeb236f3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 8 Jul 2025 16:22:11 +0300 Subject: [PATCH 01/43] fix(tab): editor not focused after switching tabs --- apps/client/src/widgets/type_widgets/type_widget.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/client/src/widgets/type_widgets/type_widget.ts b/apps/client/src/widgets/type_widgets/type_widget.ts index 3a437e109..9cd8a64ab 100644 --- a/apps/client/src/widgets/type_widgets/type_widget.ts +++ b/apps/client/src/widgets/type_widgets/type_widget.ts @@ -71,6 +71,15 @@ export default abstract class TypeWidget extends NoteContextAwareWidget { } } + activeNoteChangedEvent() { + if (!this.isActiveNoteContext()) { + return; + } + + // Restore focus to the editor when switching tabs. + this.focus(); + } + /** * {@inheritdoc} * From e4a2a8e56d3a337b6b5c9fad0ca980b76a480147 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 8 Jul 2025 16:23:58 +0300 Subject: [PATCH 02/43] fix(text): selection and cursor not maintained properly when switching tabs --- apps/client/src/widgets/type_widgets/editable_text.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/editable_text.ts b/apps/client/src/widgets/type_widgets/editable_text.ts index 3bb5754a2..e1163e2b5 100644 --- a/apps/client/src/widgets/type_widgets/editable_text.ts +++ b/apps/client/src/widgets/type_widgets/editable_text.ts @@ -265,7 +265,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } focus() { - this.$editor.trigger("focus"); + const editor = this.watchdog.editor; + if (editor) { + editor.editing.view.focus(); + } else { + this.$editor.trigger("focus"); + } } scrollToEnd() { From 71863752cdc7f39bd316b1625d4b12af27449878 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 10:25:32 +0300 Subject: [PATCH 03/43] feat(views/table): display both promoted and non-promoted attributes --- .../widgets/view_widgets/table_view/columns.ts | 4 ++-- .../widgets/view_widgets/table_view/index.ts | 8 ++++---- .../widgets/view_widgets/table_view/rows.ts | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 14 deletions(-) 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 bae64bda3..69349b1cc 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -6,7 +6,7 @@ import { LabelType } from "../../../services/promoted_attribute_definition_parse type ColumnType = LabelType | "relation"; -export interface PromotedAttributeInformation { +export interface AttributeDefinitionInformation { name: string; title?: string; type?: ColumnType; @@ -42,7 +42,7 @@ const labelTypeMappings: Record> = { } }; -export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) { +export function buildColumnDefinitions(info: AttributeDefinitionInformation[], existingColumnData?: ColumnDefinition[]) { const columnDefs: ColumnDefinition[] = [ { title: "#", 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 b6ec8ae13..085b46043 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -11,7 +11,7 @@ import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; import buildFooter from "./footer.js"; -import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js"; +import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js"; import { buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; @@ -121,7 +121,7 @@ export default class TableView extends ViewMode { private async initialize(el: HTMLElement) { const notes = await froca.getNotes(this.args.noteIds); - const info = getPromotedAttributeInformation(this.parentNote); + const info = getAttributeDefinitionInformation(this.parentNote); const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; @@ -245,7 +245,7 @@ export default class TableView extends ViewMode { return; } - const info = getPromotedAttributeInformation(this.parentNote); + const info = getAttributeDefinitionInformation(this.parentNote); const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns); this.api.setColumns(columnDefs); } @@ -256,7 +256,7 @@ export default class TableView extends ViewMode { } const notes = await froca.getNotes(this.args.noteIds); - const info = getPromotedAttributeInformation(this.parentNote); + const info = getAttributeDefinitionInformation(this.parentNote); this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info)); if (this.noteIdToEdit) { 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 288274408..4aa356581 100644 --- a/apps/client/src/widgets/view_widgets/table_view/rows.ts +++ b/apps/client/src/widgets/view_widgets/table_view/rows.ts @@ -1,6 +1,6 @@ import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; -import type { PromotedAttributeInformation } from "./columns.js"; +import type { AttributeDefinitionInformation } from "./columns.js"; export type TableData = { iconClass: string; @@ -11,7 +11,7 @@ export type TableData = { branchId: string; }; -export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { +export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: AttributeDefinitionInformation[]) { const definitions: TableData[] = []; for (const branch of parentNote.getChildBranches()) { const note = await branch.getNote(); @@ -41,17 +41,19 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf return definitions; } -export default function getPromotedAttributeInformation(parentNote: FNote) { - const info: PromotedAttributeInformation[] = []; - for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { - const def = promotedAttribute.getDefinition(); +export default function getAttributeDefinitionInformation(parentNote: FNote) { + const info: AttributeDefinitionInformation[] = []; + const attrDefs = parentNote.getAttributes() + .filter(attr => attr.isDefinition()); + for (const attrDef of attrDefs) { + const def = attrDef.getDefinition(); if (def.multiplicity !== "single") { console.warn("Multiple values are not supported for now"); continue; } - const [ labelType, name ] = promotedAttribute.name.split(":", 2); - if (promotedAttribute.type !== "label") { + const [ labelType, name ] = attrDef.name.split(":", 2); + if (attrDef.type !== "label") { console.warn("Relations are not supported for now"); continue; } From e30478e5d4985959c9eb6aafc9dc32f650bb7abd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 10:45:01 +0300 Subject: [PATCH 04/43] chore(views/table): disable menu module since it's no longer necessary --- apps/client/src/widgets/view_widgets/table_view/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 085b46043..a7b0d1e5e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -111,7 +111,7 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, MenuModule]; + const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule ]; for (const module of modules) { Tabulator.registerModule(module); } From d77a49857b0a53aacf2dc90710f56e2e975805af Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 11:06:56 +0300 Subject: [PATCH 05/43] feat(views/table): basic nested tree support --- .../widgets/view_widgets/table_view/index.ts | 27 ++++++++++--------- .../widgets/view_widgets/table_view/rows.ts | 23 ++++++++++++---- 2 files changed, 32 insertions(+), 18 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 a7b0d1e5e..b77c64f9e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -6,14 +6,15 @@ import SpacedUpdate from "../../../services/spaced_update.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule, MoveRowsModule, ColumnDefinition} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; import buildFooter from "./footer.js"; -import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js"; -import { buildColumnDefinitions } from "./columns.js"; +import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows.js"; +import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; +import { Unwrapped } from "knockout"; const TPL = /*html*/`
@@ -111,32 +112,32 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule ]; + const info = getAttributeDefinitionInformation(this.parentNote); + const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]; for (const module of modules) { Tabulator.registerModule(module); } - this.initialize(el); + this.initialize(el, info); } - private async initialize(el: HTMLElement) { - const notes = await froca.getNotes(this.args.noteIds); - const info = getAttributeDefinitionInformation(this.parentNote); - + private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) { const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; const columnDefs = buildColumnDefinitions(info); - const movableRows = canReorderRows(this.parentNote); + const { definitions: rowData, hasChildren } = await buildRowDefinitions(this.parentNote, info); + const movableRows = canReorderRows(this.parentNote) && !hasChildren; this.api = new Tabulator(el, { layout: "fitDataFill", index: "noteId", columns: columnDefs, - data: await buildRowDefinitions(this.parentNote, notes, info), + data: rowData, persistence: true, movableColumns: true, movableRows, + dataTree: hasChildren, footerElement: buildFooter(this.parentNote), persistenceWriterFunc: (_id, type: string, data: object) => { (this.persistentData as Record)[type] = data; @@ -255,9 +256,9 @@ export default class TableView extends ViewMode { return; } - const notes = await froca.getNotes(this.args.noteIds); const info = getAttributeDefinitionInformation(this.parentNote); - this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info)); + const { definitions } = await buildRowDefinitions(this.parentNote, info); + this.api.replaceData(definitions); if (this.noteIdToEdit) { const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit); 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 4aa356581..f2e355516 100644 --- a/apps/client/src/widgets/view_widgets/table_view/rows.ts +++ b/apps/client/src/widgets/view_widgets/table_view/rows.ts @@ -9,10 +9,12 @@ export type TableData = { labels: Record; relations: Record; branchId: string; + _children?: TableData[]; }; -export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: AttributeDefinitionInformation[]) { +export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[]) { const definitions: TableData[] = []; + let hasChildren = false; for (const branch of parentNote.getChildBranches()) { const note = await branch.getNote(); if (!note) { @@ -28,17 +30,28 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf labels[name] = note.getLabelValue(name); } } - definitions.push({ + + const def: TableData = { iconClass: note.getIcon(), noteId: note.noteId, title: note.title, labels, relations, - branchId: branch.branchId - }); + branchId: branch.branchId, + } + + if (note.hasChildren()) { + def._children = (await buildRowDefinitions(note, infos)).definitions; + hasChildren = true; + } + + definitions.push(def); } - return definitions; + return { + definitions, + hasChildren + }; } export default function getAttributeDefinitionInformation(parentNote: FNote) { From ccd935b562a9297a760e7e010ca9bde45026a7b6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 11:18:59 +0300 Subject: [PATCH 06/43] refactor(views/table): don't configure reordering rows if not available --- apps/client/src/widgets/view_widgets/table_view/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 b77c64f9e..713417a46 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -14,7 +14,6 @@ import buildFooter from "./footer.js"; import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows.js"; import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; -import { Unwrapped } from "knockout"; const TPL = /*html*/`
@@ -145,7 +144,10 @@ export default class TableView extends ViewMode { }, persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type], }); - configureReorderingRows(this.api); + + if (movableRows) { + configureReorderingRows(this.api); + } setupContextMenu(this.api, this.parentNote); this.setupEditing(); } From 8d29c5fe1b3781967e4b25054174322c83227e83 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 11:29:14 +0300 Subject: [PATCH 07/43] feat(views/table): hide draggable rows if not supported --- apps/client/src/widgets/view_widgets/table_view/columns.ts | 6 +++--- .../src/widgets/view_widgets/table_view/formatters.ts | 7 ++++++- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) 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 69349b1cc..5bfc51b8e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -1,7 +1,7 @@ import { RelationEditor } from "./relation_editor.js"; import { NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; import { applyHeaderMenu } from "./header-menu.js"; -import type { ColumnDefinition } from "tabulator-tables"; +import type { ColumnDefinition, Tabulator } from "tabulator-tables"; import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; type ColumnType = LabelType | "relation"; @@ -42,7 +42,7 @@ const labelTypeMappings: Record> = { } }; -export function buildColumnDefinitions(info: AttributeDefinitionInformation[], existingColumnData?: ColumnDefinition[]) { +export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[]) { const columnDefs: ColumnDefinition[] = [ { title: "#", @@ -50,7 +50,7 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], e hozAlign: "center", resizable: false, frozen: true, - rowHandle: true, + rowHandle: movableRows, formatter: RowNumberFormatter }, { diff --git a/apps/client/src/widgets/view_widgets/table_view/formatters.ts b/apps/client/src/widgets/view_widgets/table_view/formatters.ts index 1994ab236..136539652 100644 --- a/apps/client/src/widgets/view_widgets/table_view/formatters.ts +++ b/apps/client/src/widgets/view_widgets/table_view/formatters.ts @@ -37,7 +37,12 @@ export function NoteTitleFormatter(cell: CellComponent) { } export function RowNumberFormatter(cell: CellComponent) { - return ` ` + cell.getRow().getPosition(true); + let html = ""; + if (cell.getColumn().getDefinition().rowHandle) { + html += ` `; + } + html += cell.getRow().getPosition(true); + return html; } function buildNoteLink(noteId: string) { 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 713417a46..1c3e65d24 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -124,9 +124,9 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; - const columnDefs = buildColumnDefinitions(info); const { definitions: rowData, hasChildren } = await buildRowDefinitions(this.parentNote, info); const movableRows = canReorderRows(this.parentNote) && !hasChildren; + const columnDefs = buildColumnDefinitions(info, movableRows); this.api = new Tabulator(el, { layout: "fitDataFill", @@ -249,7 +249,7 @@ export default class TableView extends ViewMode { } const info = getAttributeDefinitionInformation(this.parentNote); - const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns); + const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns); this.api.setColumns(columnDefs); } From 28f4aea3d5e3fa7b4b4501137f8f626619c1a546 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 11:30:46 +0300 Subject: [PATCH 08/43] refactor(views/table): use slightly more performant formatter for row number --- .../widgets/view_widgets/table_view/columns.ts | 2 +- .../view_widgets/table_view/formatters.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) 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 5bfc51b8e..ca0adc2f0 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -51,7 +51,7 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m resizable: false, frozen: true, rowHandle: movableRows, - formatter: RowNumberFormatter + formatter: RowNumberFormatter(movableRows) }, { field: "noteId", diff --git a/apps/client/src/widgets/view_widgets/table_view/formatters.ts b/apps/client/src/widgets/view_widgets/table_view/formatters.ts index 136539652..15701bc5c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/formatters.ts +++ b/apps/client/src/widgets/view_widgets/table_view/formatters.ts @@ -36,13 +36,15 @@ export function NoteTitleFormatter(cell: CellComponent) { return $noteRef[0].outerHTML; } -export function RowNumberFormatter(cell: CellComponent) { - let html = ""; - if (cell.getColumn().getDefinition().rowHandle) { - html += ` `; - } - html += cell.getRow().getPosition(true); - return html; +export function RowNumberFormatter(draggableRows: boolean) { + return (cell: CellComponent) => { + let html = ""; + if (draggableRows) { + html += ` `; + } + html += cell.getRow().getPosition(true); + return html; + }; } function buildNoteLink(noteId: string) { From 5f9a6a9f767d7733d503b79371fdc713701f73db Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 11:39:12 +0300 Subject: [PATCH 09/43] feat(views/table): integrate expander into note title section --- apps/client/src/widgets/view_widgets/table_view/index.ts | 1 + 1 file changed, 1 insertion(+) 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 1c3e65d24..0e83f0bc8 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -137,6 +137,7 @@ export default class TableView extends ViewMode { movableColumns: true, movableRows, dataTree: hasChildren, + dataTreeElementColumn: "title", footerElement: buildFooter(this.parentNote), persistenceWriterFunc: (_id, type: string, data: object) => { (this.persistentData as Record)[type] = data; From ec7dacfc9b3b6f61f75d0effce3ee33acf5514ad Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 12:04:13 +0300 Subject: [PATCH 10/43] feat(views/table): improve expand/collapse button --- .../widgets/view_widgets/table_view/index.ts | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 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 0e83f0bc8..40d431ad0 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -6,7 +6,7 @@ import SpacedUpdate from "../../../services/spaced_update.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; @@ -65,6 +65,26 @@ const TPL = /*html*/` justify-content: left; gap: 0.5em; } + + .tabulator button.tree-expand, + .tabulator button.tree-collapse { + display: inline-block; + appearance: none; + border: 0; + background: transparent; + width: 1.5em; + position: relative; + vertical-align: middle; + } + + .tabulator button.tree-expand span, + .tabulator button.tree-collapse span { + position: absolute; + top: 0; + left: 0; + font-size: 1.5em; + transform: translateY(-50%); + }
@@ -127,8 +147,7 @@ export default class TableView extends ViewMode { const { definitions: rowData, hasChildren } = await buildRowDefinitions(this.parentNote, info); const movableRows = canReorderRows(this.parentNote) && !hasChildren; const columnDefs = buildColumnDefinitions(info, movableRows); - - this.api = new Tabulator(el, { + let opts: Options = { layout: "fitDataFill", index: "noteId", columns: columnDefs, @@ -136,16 +155,25 @@ export default class TableView extends ViewMode { persistence: true, movableColumns: true, movableRows, - dataTree: hasChildren, - dataTreeElementColumn: "title", footerElement: buildFooter(this.parentNote), persistenceWriterFunc: (_id, type: string, data: object) => { (this.persistentData as Record)[type] = data; this.spacedUpdate.scheduleUpdate(); }, persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type], - }); + }; + if (hasChildren) { + opts = { + ...opts, + dataTree: hasChildren, + dataTreeElementColumn: "title", + dataTreeExpandElement: ``, + dataTreeCollapseElement: `` + } + } + + this.api = new Tabulator(el, opts); if (movableRows) { configureReorderingRows(this.api); } From b29c3eff6e7591aa953d6013db34168a670b3420 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 12:53:11 +0300 Subject: [PATCH 11/43] refactor(views): prepare for supporting subtrees --- apps/client/src/entities/fnote.ts | 8 ++++++++ apps/client/src/widgets/view_widgets/table_view/index.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index b5d575ed9..15ba79c22 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -256,6 +256,14 @@ class FNote { return this.children; } + async getSubtreeNoteIds() { + let noteIds: (string | string[])[] = []; + for (const child of await this.getChildNotes()) { + noteIds.push(await child.getSubtreeNoteIds()); + } + return noteIds.flat(); + } + async getChildNotes() { return await this.froca.getNotes(this.children); } 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 40d431ad0..023af3e42 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -263,7 +263,7 @@ export default class TableView extends ViewMode { this.#manageColumnUpdate(); } - if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId) + if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.args.noteIds.includes(branch.parentNoteId ?? "")) || loadResults.getNoteIds().some(noteId => this.args.noteIds.includes(noteId) || loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!)))) { this.#manageRowsUpdate(); From 8c56315313d241256ca1d87c6aa3d7b9777b4c8a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 12:56:17 +0300 Subject: [PATCH 12/43] refactor(views): move full height detection to rendererer --- apps/client/src/services/note_list_renderer.ts | 8 +++++++- apps/client/src/widgets/view_widgets/calendar_view.ts | 4 ---- apps/client/src/widgets/view_widgets/geo_view/index.ts | 4 ---- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 ---- apps/client/src/widgets/view_widgets/view_mode.ts | 5 ----- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index 122ba9745..1f9719b8e 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -47,7 +47,13 @@ export default class NoteListRenderer { } get isFullHeight() { - return this.viewMode?.isFullHeight; + switch (this.viewType) { + case "list": + case "grid": + return false; + default: + return true; + } } async renderList() { diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index c8f0119f6..3fb304e3d 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -129,10 +129,6 @@ export default class CalendarView extends ViewMode<{}> { args.$parent.append(this.$root); } - get isFullHeight(): boolean { - return true; - } - async renderList(): Promise | undefined> { this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); const isEditable = !this.isCalendarRoot; diff --git a/apps/client/src/widgets/view_widgets/geo_view/index.ts b/apps/client/src/widgets/view_widgets/geo_view/index.ts index 4d35fd85a..758123ced 100644 --- a/apps/client/src/widgets/view_widgets/geo_view/index.ts +++ b/apps/client/src/widgets/view_widgets/geo_view/index.ts @@ -243,10 +243,6 @@ export default class GeoView extends ViewMode { } } - get isFullHeight(): boolean { - return true; - } - #changeState(newState: State) { this._state = newState; this.$container.toggleClass("placing-note", newState === State.NewNote); 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 023af3e42..fd6da3781 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -120,10 +120,6 @@ export default class TableView extends ViewMode { args.$parent.append(this.$root); } - get isFullHeight(): boolean { - return true; - } - async renderList() { this.$container.empty(); this.renderTable(this.$container[0]); diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index f3706da4a..3115454db 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -39,11 +39,6 @@ export default abstract class ViewMode extends Component { // Do nothing by default. } - get isFullHeight() { - // Override to change its value. - return false; - } - get isReadOnly() { return this.parentNote.hasLabel("readOnly"); } From 402540f48326461fc7c64e51ee65852598519e0d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 13:12:54 +0300 Subject: [PATCH 13/43] feat(views/table): support recursive children update --- apps/client/src/entities/fnote.ts | 1 + .../client/src/services/note_list_renderer.ts | 56 +++++++++++-------- apps/client/src/widgets/note_list.ts | 3 +- apps/client/src/widgets/search_result.ts | 1 - 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 15ba79c22..ad5f0e556 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -259,6 +259,7 @@ class FNote { async getSubtreeNoteIds() { let noteIds: (string | string[])[] = []; for (const child of await this.getChildNotes()) { + noteIds.push(child.noteId); noteIds.push(await child.getSubtreeNoteIds()); } return noteIds.flat(); diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index 1f9719b8e..8e4a6312e 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -6,33 +6,18 @@ import TableView from "../widgets/view_widgets/table_view/index.js"; import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; import type ViewMode from "../widgets/view_widgets/view_mode.js"; +export type ArgsWithoutNoteId = Omit; export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap"; export default class NoteListRenderer { private viewType: ViewTypeOptions; - public viewMode: ViewMode | null; + private args: ArgsWithoutNoteId; + public viewMode?: ViewMode; - constructor(args: ViewModeArgs) { + constructor(args: ArgsWithoutNoteId) { + this.args = args; this.viewType = this.#getViewType(args.parentNote); - - switch (this.viewType) { - case "list": - case "grid": - this.viewMode = new ListOrGridView(this.viewType, args); - break; - case "calendar": - this.viewMode = new CalendarView(args); - break; - case "table": - this.viewMode = new TableView(args); - break; - case "geoMap": - this.viewMode = new GeoView(args); - break; - default: - this.viewMode = null; - } } #getViewType(parentNote: FNote): ViewTypeOptions { @@ -57,11 +42,36 @@ export default class NoteListRenderer { } async renderList() { - if (!this.viewMode) { - return null; + const args = this.args; + + let noteIds: string[]; + if (this.viewType === "list" || this.viewType === "grid") { + noteIds = args.parentNote.getChildNoteIds(); + } else { + noteIds = await args.parentNote.getSubtreeNoteIds(); } - return await this.viewMode.renderList(); + const viewMode = this.#buildViewMode({ + ...args, + noteIds + }); + this.viewMode = viewMode; + return await viewMode.renderList(); + } + + #buildViewMode(args: ViewModeArgs) { + switch (this.viewType) { + case "calendar": + return new CalendarView(args); + case "table": + return new TableView(args); + case "geoMap": + return new GeoView(args); + case "list": + case "grid": + default: + return new ListOrGridView(this.viewType, args); + } } } diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index 68687e63c..65015039a 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -123,8 +123,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { const noteListRenderer = new NoteListRenderer({ $parent: this.$content, parentNote: note, - parentNotePath: this.notePath, - noteIds: note.getChildNoteIds() + parentNotePath: this.notePath }); this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); await noteListRenderer.renderList(); diff --git a/apps/client/src/widgets/search_result.ts b/apps/client/src/widgets/search_result.ts index a98e306b3..6fa69ae13 100644 --- a/apps/client/src/widgets/search_result.ts +++ b/apps/client/src/widgets/search_result.ts @@ -68,7 +68,6 @@ export default class SearchResultWidget extends NoteContextAwareWidget { const noteListRenderer = new NoteListRenderer({ $parent: this.$content, parentNote: note, - noteIds: note.getChildNoteIds(), showNotePath: true }); await noteListRenderer.renderList(); From c13969217c2945139c21629efdc0269c80258e44 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 13:37:18 +0300 Subject: [PATCH 14/43] feat(views/table): insert child note --- .../src/translations/en/translation.json | 3 ++- .../view_widgets/table_view/context_menu.ts | 18 ++++++++++++++++++ .../widgets/view_widgets/table_view/index.ts | 4 ++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 637b1fb49..4cb8bbefe 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1952,7 +1952,8 @@ "hide-column": "Hide column \"{{title}}\"", "show-hide-columns": "Show/hide columns", "row-insert-above": "Insert row above", - "row-insert-below": "Insert row below" + "row-insert-below": "Insert row below", + "row-insert-child": "Insert child note" }, "book_properties_config": { "hide-weekends": "Hide weekends", 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 55fb75002..4eff60a04 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 @@ -5,6 +5,7 @@ import branches from "../../../services/branches.js"; import { t } from "../../../services/i18n.js"; import link_context_menu from "../../../menus/link_context_menu.js"; import type FNote from "../../../entities/fnote.js"; +import froca from "../../../services/froca.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote)); @@ -118,6 +119,23 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F }); } }, + { + title: t("table_view.row-insert-child"), + uiIcon: "bx bx-empty", + handler: async () => { + const branchId = row.getData().branchId; + const note = await froca.getBranch(branchId)?.getNote(); + const bestNotePath = note?.getBestNotePath(parentNote.noteId); + const target = e.target; + if (!target) { + return; + } + const component = $(target).closest(".component").prop("component"); + component.triggerCommand("addNewRow", { + parentNotePath: bestNotePath?.join("/") + }); + } + }, { title: t("table_view.row-insert-below"), uiIcon: "bx bx-empty", 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 fd6da3781..9dc361722 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -229,8 +229,8 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } - addNewRowCommand({ customOpts }: { customOpts: CreateNoteOpts }) { - const parentNotePath = this.args.parentNotePath; + addNewRowCommand({ customOpts, parentNotePath: customNotePath }: { customOpts: CreateNoteOpts, parentNotePath?: string }) { + const parentNotePath = customNotePath ?? this.args.parentNotePath; if (parentNotePath) { const opts: CreateNoteOpts = { activate: false, From 84479a2c2a09488b3a94733b0f8b72ab8c3f1331 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 15:38:57 +0300 Subject: [PATCH 15/43] feat(views/table): focus if creating child note --- .../view_widgets/table_view/context_menu.ts | 11 ++-- .../widgets/view_widgets/table_view/index.ts | 56 ++++++++++++++----- 2 files changed, 49 insertions(+), 18 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 4eff60a04..70054d2ea 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 @@ -123,16 +123,19 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F title: t("table_view.row-insert-child"), uiIcon: "bx bx-empty", handler: async () => { - const branchId = row.getData().branchId; - const note = await froca.getBranch(branchId)?.getNote(); - const bestNotePath = note?.getBestNotePath(parentNote.noteId); const target = e.target; if (!target) { return; } + const branchId = row.getData().branchId; + const note = await froca.getBranch(branchId)?.getNote(); const component = $(target).closest(".component").prop("component"); component.triggerCommand("addNewRow", { - parentNotePath: bestNotePath?.join("/") + parentNotePath: note?.noteId, + customOpts: { + target: "after", + targetBranchId: branchId, + } }); } }, 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 9dc361722..03f1a25fd 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -6,7 +6,7 @@ import SpacedUpdate from "../../../services/spaced_update.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; @@ -107,7 +107,7 @@ export default class TableView extends ViewMode { private newAttribute?: Attribute; private persistentData: StateInfo["tableData"]; /** If set to a note ID, whenever the rows will be updated, the title of the note will be automatically focused for editing. */ - private noteIdToEdit?: string; + private branchIdToEdit?: string; constructor(args: ViewModeArgs) { super(args, "table"); @@ -145,7 +145,7 @@ export default class TableView extends ViewMode { const columnDefs = buildColumnDefinitions(info, movableRows); let opts: Options = { layout: "fitDataFill", - index: "noteId", + index: "branchId", columns: columnDefs, data: rowData, persistence: true, @@ -237,11 +237,12 @@ export default class TableView extends ViewMode { ...customOpts } console.log("Create with ", opts); - note_create.createNote(parentNotePath, opts).then(({ note }) => { - if (!note) { - return; + note_create.createNote(parentNotePath, opts).then(({ branch }) => { + if (branch) { + setTimeout(() => { + this.focusOnBranch(branch?.branchId); + }); } - this.noteIdToEdit = note.noteId; }) } } @@ -285,16 +286,43 @@ export default class TableView extends ViewMode { const info = getAttributeDefinitionInformation(this.parentNote); const { definitions } = await buildRowDefinitions(this.parentNote, info); - this.api.replaceData(definitions); + await this.api.replaceData(definitions); + } - if (this.noteIdToEdit) { - const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit); - if (row) { - row.getCell("title").edit(); - } - this.noteIdToEdit = undefined; + focusOnBranch(branchId: string) { + if (!this.api) { + return; } + + const row = findRowDataById(this.api.getRows(), branchId); + if (!row) { + return; + } + + // Expand the parent tree if any. + if (this.api.options.dataTree) { + const parent = row.getTreeParent(); + if (parent) { + parent.treeExpand(); + } + } + + row.getCell("title").edit(); } } + +function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { + for (let row of rows) { + const item = row.getIndex() as string; + + if (item === branchId) { + return row; + } + + let found = findRowDataById(row.getTreeChildren(), branchId); + if (found) return found; + } + return null; +} From e703ce92a877ae0686a9791cab7a338a4dad09c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 15:46:22 +0300 Subject: [PATCH 16/43] refactor(views/table): simplify context menu handling --- apps/client/src/components/app_context.ts | 7 +++ .../view_widgets/table_view/context_menu.ts | 52 ++++++++----------- .../widgets/view_widgets/table_view/index.ts | 3 +- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 0c155c1e8..bbd124864 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -28,6 +28,7 @@ import TouchBarComponent from "./touch_bar.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; import { StartupChecks } from "./startup_checks.js"; +import type { CreateNoteOpts } from "../services/note_create.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; @@ -276,6 +277,12 @@ export type CommandMappings = { geoMapCreateChildNote: CommandData; + // Table view + addNewRow: CommandData & { + customOpts: CreateNoteOpts; + parentNotePath?: string; + }; + buildTouchBar: CommandData & { TouchBar: typeof TouchBar; buildIcon(name: string): NativeImage; 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 70054d2ea..910c65895 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 @@ -6,6 +6,7 @@ import { t } from "../../../services/i18n.js"; 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"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote)); @@ -105,32 +106,20 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F { title: t("table_view.row-insert-above"), uiIcon: "bx bx-list-plus", - handler: () => { - const target = e.target; - if (!target) { - return; + handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + customOpts: { + target: "before", + targetBranchId: rowData.branchId, } - const component = $(target).closest(".component").prop("component"); - component.triggerCommand("addNewRow", { - customOpts: { - target: "before", - targetBranchId: rowData.branchId, - } - }); - } + }) }, { title: t("table_view.row-insert-child"), uiIcon: "bx bx-empty", handler: async () => { - const target = e.target; - if (!target) { - return; - } const branchId = row.getData().branchId; const note = await froca.getBranch(branchId)?.getNote(); - const component = $(target).closest(".component").prop("component"); - component.triggerCommand("addNewRow", { + getParentComponent(e)?.triggerCommand("addNewRow", { parentNotePath: note?.noteId, customOpts: { target: "after", @@ -142,19 +131,12 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F { title: t("table_view.row-insert-below"), uiIcon: "bx bx-empty", - handler: () => { - const target = e.target; - if (!target) { - return; + handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + customOpts: { + target: "after", + targetBranchId: rowData.branchId, } - const component = $(target).closest(".component").prop("component"); - component.triggerCommand("addNewRow", { - customOpts: { - target: "after", - targetBranchId: rowData.branchId, - } - }); - } + }) }, { title: "----" }, { @@ -169,3 +151,13 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F }); e.preventDefault(); } + +function getParentComponent(e: MouseEvent) { + if (!e.target) { + return; + } + + return $(e.target) + .closest(".component") + .prop("component") as Component; +} 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 03f1a25fd..bcb674420 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -229,14 +229,13 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } - addNewRowCommand({ customOpts, parentNotePath: customNotePath }: { customOpts: CreateNoteOpts, parentNotePath?: string }) { + addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { const parentNotePath = customNotePath ?? this.args.parentNotePath; if (parentNotePath) { const opts: CreateNoteOpts = { activate: false, ...customOpts } - console.log("Create with ", opts); note_create.createNote(parentNotePath, opts).then(({ branch }) => { if (branch) { setTimeout(() => { From cd338085fb5023f88618a79a81e8b1fe389766ce Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 15:52:17 +0300 Subject: [PATCH 17/43] refactor(views/table): clean up --- .../view_widgets/table_view/columns.ts | 2 - .../view_widgets/table_view/header-menu.ts | 53 ------------------- .../widgets/view_widgets/table_view/index.ts | 2 - 3 files changed, 57 deletions(-) delete mode 100644 apps/client/src/widgets/view_widgets/table_view/header-menu.ts 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 ca0adc2f0..db42425c5 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -1,6 +1,5 @@ import { RelationEditor } from "./relation_editor.js"; import { NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; -import { applyHeaderMenu } from "./header-menu.js"; import type { ColumnDefinition, Tabulator } from "tabulator-tables"; import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; @@ -86,7 +85,6 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m seenFields.add(field); } - applyHeaderMenu(columnDefs); if (existingColumnData) { restoreExistingData(columnDefs, existingColumnData); } diff --git a/apps/client/src/widgets/view_widgets/table_view/header-menu.ts b/apps/client/src/widgets/view_widgets/table_view/header-menu.ts deleted file mode 100644 index 7f098fa99..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/header-menu.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ColumnComponent, ColumnDefinition, MenuObject, Tabulator } from "tabulator-tables"; - -export function applyHeaderMenu(columns: ColumnDefinition[]) { - for (let column of columns) { - if (column.headerSort !== false) { - column.headerMenu = headerMenu; - } - } -} - -function headerMenu(this: Tabulator) { - const menu: MenuObject[] = []; - const columns = this.getColumns(); - - for (let column of columns) { - //create checkbox element using font awesome icons - let icon = document.createElement("i"); - icon.classList.add("bx"); - icon.classList.add(column.isVisible() ? "bx-check" : "bx-empty"); - - //build label - let label = document.createElement("span"); - let title = document.createElement("span"); - - title.textContent = " " + column.getDefinition().title; - - label.appendChild(icon); - label.appendChild(title); - - //create menu item - menu.push({ - label: label, - action: function (e) { - //prevent menu closing - e.stopPropagation(); - - //toggle current column visibility - column.toggle(); - - //change menu item icon - if (column.isVisible()) { - icon.classList.remove("bx-empty"); - icon.classList.add("bx-check"); - } else { - icon.classList.remove("bx-check"); - icon.classList.add("bx-empty"); - } - } - }); - } - - return menu; -}; 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 bcb674420..4ab836685 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -106,8 +106,6 @@ export default class TableView extends ViewMode { private api?: Tabulator; private newAttribute?: Attribute; private persistentData: StateInfo["tableData"]; - /** If set to a note ID, whenever the rows will be updated, the title of the note will be automatically focused for editing. */ - private branchIdToEdit?: string; constructor(args: ViewModeArgs) { super(args, "table"); From caa842cd55065a1c4465da4c41002983077f481e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:16:55 +0300 Subject: [PATCH 18/43] fix(views/table): unable to update state for newly created rows --- .../client/src/services/note_list_renderer.ts | 14 ++------- apps/client/src/widgets/note_list.ts | 6 ---- .../src/widgets/view_widgets/calendar_view.ts | 2 -- .../widgets/view_widgets/list_or_grid_view.ts | 25 ++++++++-------- .../widgets/view_widgets/table_view/index.ts | 8 ++--- .../src/widgets/view_widgets/view_mode.ts | 30 ++++++++++++++++++- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index 8e4a6312e..08af048f0 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -43,19 +43,9 @@ export default class NoteListRenderer { async renderList() { const args = this.args; - - let noteIds: string[]; - if (this.viewType === "list" || this.viewType === "grid") { - noteIds = args.parentNote.getChildNoteIds(); - } else { - noteIds = await args.parentNote.getSubtreeNoteIds(); - } - - const viewMode = this.#buildViewMode({ - ...args, - noteIds - }); + const viewMode = this.#buildViewMode(args); this.viewMode = viewMode; + await viewMode.beforeRender(); return await viewMode.renderList(); } diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index 65015039a..1b00bd8c2 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -168,12 +168,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { this.refresh(); this.checkRenderStatus(); } - - // Inform the view mode of changes and refresh if needed. - if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) { - this.refresh(); - this.checkRenderStatus(); - } } buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) { diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index 3fb304e3d..d3f20d565 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -113,7 +113,6 @@ export default class CalendarView extends ViewMode<{}> { private $root: JQuery; private $calendarContainer: JQuery; - private noteIds: string[]; private calendar?: Calendar; private isCalendarRoot: boolean; private lastView?: string; @@ -124,7 +123,6 @@ export default class CalendarView extends ViewMode<{}> { this.$root = $(TPL); this.$calendarContainer = this.$root.find(".calendar-container"); - this.noteIds = args.noteIds; this.isCalendarRoot = false; args.$parent.append(this.$root); } diff --git a/apps/client/src/widgets/view_widgets/list_or_grid_view.ts b/apps/client/src/widgets/view_widgets/list_or_grid_view.ts index c8236b053..1bfc029ab 100644 --- a/apps/client/src/widgets/view_widgets/list_or_grid_view.ts +++ b/apps/client/src/widgets/view_widgets/list_or_grid_view.ts @@ -161,7 +161,7 @@ const TPL = /*html*/` class ListOrGridView extends ViewMode<{}> { private $noteList: JQuery; - private noteIds: string[]; + private filteredNoteIds!: string[]; private page?: number; private pageSize?: number; private showNotePath?: boolean; @@ -174,13 +174,6 @@ class ListOrGridView extends ViewMode<{}> { super(args, viewType); this.$noteList = $(TPL); - const includedNoteIds = this.getIncludedNoteIds(); - - this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); - - if (this.noteIds.length === 0) { - return; - } args.$parent.append(this.$noteList); @@ -204,8 +197,14 @@ class ListOrGridView extends ViewMode<{}> { return new Set(includedLinks.map((rel) => rel.value)); } + async beforeRender() { + super.beforeRender(); + const includedNoteIds = this.getIncludedNoteIds(); + this.filteredNoteIds = this.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); + } + async renderList() { - if (this.noteIds.length === 0 || !this.page || !this.pageSize) { + if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) { this.$noteList.hide(); return; } @@ -226,7 +225,7 @@ class ListOrGridView extends ViewMode<{}> { const startIdx = (this.page - 1) * this.pageSize; const endIdx = startIdx + this.pageSize; - const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); + const pageNoteIds = this.filteredNoteIds.slice(startIdx, Math.min(endIdx, this.filteredNoteIds.length)); const pageNotes = await froca.getNotes(pageNoteIds); for (const note of pageNotes) { @@ -246,7 +245,7 @@ class ListOrGridView extends ViewMode<{}> { return; } - const pageCount = Math.ceil(this.noteIds.length / this.pageSize); + const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize); $pager.toggle(pageCount > 1); @@ -257,7 +256,7 @@ class ListOrGridView extends ViewMode<{}> { lastPrinted = true; const startIndex = (i - 1) * this.pageSize + 1; - const endIndex = Math.min(this.noteIds.length, i * this.pageSize); + const endIndex = Math.min(this.filteredNoteIds.length, i * this.pageSize); $pager.append( i === this.page @@ -279,7 +278,7 @@ class ListOrGridView extends ViewMode<{}> { } // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all - $pager.append(`(${this.noteIds.length} notes)`); + $pager.append(`(${this.filteredNoteIds.length} notes)`); } async renderNote(note: FNote, expand: boolean = false) { 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 4ab836685..dac44fd3f 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -101,7 +101,6 @@ export default class TableView extends ViewMode { private $root: JQuery; private $container: JQuery; - private args: ViewModeArgs; private spacedUpdate: SpacedUpdate; private api?: Tabulator; private newAttribute?: Attribute; @@ -112,7 +111,6 @@ export default class TableView extends ViewMode { this.$root = $(TPL); this.$container = this.$root.find(".table-view-container"); - this.args = args; this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); this.persistentData = {}; args.$parent.append(this.$root); @@ -257,9 +255,9 @@ export default class TableView extends ViewMode { this.#manageColumnUpdate(); } - if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.args.noteIds.includes(branch.parentNoteId ?? "")) - || loadResults.getNoteIds().some(noteId => this.args.noteIds.includes(noteId) - || loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!)))) { + 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!)))) { this.#manageRowsUpdate(); } diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index 3115454db..4d04130f7 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -1,4 +1,5 @@ import type { EventData } from "../../components/app_context.js"; +import appContext from "../../components/app_context.js"; import Component from "../../components/component.js"; import type FNote from "../../entities/fnote.js"; import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; @@ -8,7 +9,6 @@ export interface ViewModeArgs { $parent: JQuery; parentNote: FNote; parentNotePath?: string | null; - noteIds: string[]; showNotePath?: boolean; } @@ -17,6 +17,8 @@ export default abstract class ViewMode extends Component { private _viewStorage: ViewModeStorage | null; protected parentNote: FNote; protected viewType: ViewTypeOptions; + protected noteIds: string[]; + protected args: ViewModeArgs; constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { super(); @@ -25,6 +27,12 @@ export default abstract class ViewMode extends Component { // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work args.$parent.empty(); this.viewType = viewType; + this.args = args; + this.noteIds = []; + } + + async beforeRender() { + await this.#refreshNoteIds(); } abstract renderList(): Promise | undefined>; @@ -39,6 +47,16 @@ export default abstract class ViewMode extends Component { // Do nothing by default. } + entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { + if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) { + this.#refreshNoteIds(); + } + + if (this.onEntitiesReloaded(e)) { + appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId }); + } + } + get isReadOnly() { return this.parentNote.hasLabel("readOnly"); } @@ -52,4 +70,14 @@ export default abstract class ViewMode extends Component { return this._viewStorage; } + async #refreshNoteIds() { + let noteIds: string[]; + if (this.viewType === "list" || this.viewType === "grid") { + noteIds = this.args.parentNote.getChildNoteIds(); + } else { + noteIds = await this.args.parentNote.getSubtreeNoteIds(); + } + this.noteIds = noteIds; + } + } From b255d70e188c9b33189cd150ac51e323695c8cfa Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:24:54 +0300 Subject: [PATCH 19/43] fix(views/table): context menu remains active while clicking on an expand/collapse button --- .../src/widgets/view_widgets/table_view/context_menu.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 910c65895..74f480cb7 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,13 @@ import type Component from "../../../components/component.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote)); tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, tabulator)); + + // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. + if (tabulator.options.dataTree) { + const dismissContextMenu = () => contextMenu.hide(); + tabulator.on("dataTreeRowExpanded", dismissContextMenu); + tabulator.on("dataTreeRowCollapsed", dismissContextMenu); + } } function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: Tabulator) { From 4a82c3f65a84a9f4dfbaa0ab1e8fd16d5acde2d6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:49:29 +0300 Subject: [PATCH 20/43] fix(views/table): insert above/below not working in nested trees --- .../view_widgets/table_view/context_menu.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 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 74f480cb7..6d85b63c0 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 @@ -9,7 +9,7 @@ import froca from "../../../services/froca.js"; import type Component from "../../../components/component.js"; export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) { - tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote)); + tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator)); tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, tabulator)); // Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't. @@ -103,9 +103,19 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: } } -export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote) { +export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) { const e = _e as MouseEvent; const rowData = row.getData() as TableData; + + let parentNoteId: string = parentNote.noteId; + + if (tabulator.options.dataTree) { + const parentRow = row.getTreeParent(); + if (parentRow) { + parentNoteId = parentRow.getData().noteId as string; + } + } + contextMenu.show({ items: [ ...link_context_menu.getItems(), @@ -114,6 +124,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F title: t("table_view.row-insert-above"), uiIcon: "bx bx-list-plus", handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + parentNotePath: parentNoteId, customOpts: { target: "before", targetBranchId: rowData.branchId, @@ -139,6 +150,7 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F title: t("table_view.row-insert-below"), uiIcon: "bx bx-empty", handler: () => getParentComponent(e)?.triggerCommand("addNewRow", { + parentNotePath: parentNoteId, customOpts: { target: "after", targetBranchId: rowData.branchId, From 4cc2fa5300ebb1fa97264941c0f3654685f6759c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:49:42 +0300 Subject: [PATCH 21/43] fix(snippets): warning about missing note IDs when deleting --- apps/client/src/widgets/type_widgets/ckeditor/snippets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/ckeditor/snippets.ts b/apps/client/src/widgets/type_widgets/ckeditor/snippets.ts index b2d99cfd6..7c29e58cc 100644 --- a/apps/client/src/widgets/type_widgets/ckeditor/snippets.ts +++ b/apps/client/src/widgets/type_widgets/ckeditor/snippets.ts @@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) { const templateNoteIds = new Set(templateCache.keys()); const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds); - await froca.getNotes(affectedNoteIds); + await froca.getNotes(affectedNoteIds, true); let fullReloadNeeded = false; for (const affectedTemplateNoteId of affectedTemplateNoteIds) { From 38fce25b86a30737675049b6cd7c235b3b7d4a7e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:51:20 +0300 Subject: [PATCH 22/43] fix(views/table): show/hide columns not always updated properly --- .../src/widgets/view_widgets/table_view/context_menu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 6d85b63c0..dd229ae87 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 @@ -88,11 +88,11 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: function buildColumnItems() { const items: MenuItem[] = []; for (const column of tabulator.getColumns()) { - const { title, visible, field } = column.getDefinition(); + const { title, field } = column.getDefinition(); items.push({ title, - checked: visible, + checked: column.isVisible(), uiIcon: "bx bx-empty", enabled: !!field, handler: () => column.toggle() From fcbbc21a80dc2d669324b7b3face0e771fd4cb04 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 16:58:14 +0300 Subject: [PATCH 23/43] feat(views/table): force a refresh if data tree changes --- .../src/widgets/view_widgets/calendar_view.ts | 2 +- .../src/widgets/view_widgets/geo_view/index.ts | 2 +- .../src/widgets/view_widgets/table_view/index.ts | 15 +++++++++++---- .../src/widgets/view_widgets/table_view/rows.ts | 6 +++--- apps/client/src/widgets/view_widgets/view_mode.ts | 6 +++--- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index d3f20d565..8402d1eb8 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -390,7 +390,7 @@ export default class CalendarView extends ViewMode<{}> { } } - onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { + async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { // Refresh note IDs if they got changed. if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) { this.noteIds = this.parentNote.getChildNoteIds(); diff --git a/apps/client/src/widgets/view_widgets/geo_view/index.ts b/apps/client/src/widgets/view_widgets/geo_view/index.ts index 758123ced..2ead57bde 100644 --- a/apps/client/src/widgets/view_widgets/geo_view/index.ts +++ b/apps/client/src/widgets/view_widgets/geo_view/index.ts @@ -251,7 +251,7 @@ export default class GeoView extends ViewMode { } } - onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { // If any of the children branches are altered. if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) { this.#reloadMarkers(); 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 dac44fd3f..6f7d49156 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -136,7 +136,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; - const { definitions: rowData, hasChildren } = await buildRowDefinitions(this.parentNote, info); + const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info); const movableRows = canReorderRows(this.parentNote) && !hasChildren; const columnDefs = buildColumnDefinitions(info, movableRows); let opts: Options = { @@ -242,7 +242,7 @@ export default class TableView extends ViewMode { } } - onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { if (!this.api) { return; } @@ -258,7 +258,7 @@ export default class TableView extends ViewMode { 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!)))) { - this.#manageRowsUpdate(); + return await this.#manageRowsUpdate(); } return false; @@ -280,8 +280,15 @@ export default class TableView extends ViewMode { } const info = getAttributeDefinitionInformation(this.parentNote); - const { definitions } = await buildRowDefinitions(this.parentNote, info); + const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info); + + // Force a refresh if the data tree needs enabling/disabling. + if (this.api.options.dataTree !== hasSubtree) { + return true; + } + await this.api.replaceData(definitions); + return false; } focusOnBranch(branchId: string) { 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 f2e355516..615dd7f9b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/rows.ts +++ b/apps/client/src/widgets/view_widgets/table_view/rows.ts @@ -14,7 +14,7 @@ export type TableData = { export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[]) { const definitions: TableData[] = []; - let hasChildren = false; + let hasSubtree = false; for (const branch of parentNote.getChildBranches()) { const note = await branch.getNote(); if (!note) { @@ -42,7 +42,7 @@ export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDef if (note.hasChildren()) { def._children = (await buildRowDefinitions(note, infos)).definitions; - hasChildren = true; + hasSubtree = true; } definitions.push(def); @@ -50,7 +50,7 @@ export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDef return { definitions, - hasChildren + hasSubtree }; } diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index 4d04130f7..4755294f8 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -43,16 +43,16 @@ export default abstract class ViewMode extends Component { * @param e the event data. * @return {@code true} if the view should be re-rendered, a falsy value otherwise. */ - onEntitiesReloaded(e: EventData<"entitiesReloaded">): boolean | void { + async onEntitiesReloaded(e: EventData<"entitiesReloaded">): Promise { // Do nothing by default. } - entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { + async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) { this.#refreshNoteIds(); } - if (this.onEntitiesReloaded(e)) { + if (await this.onEntitiesReloaded(e)) { appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId }); } } From ded5b1f5d279c713012808e10ae5a7595c2f4892 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 17:00:01 +0300 Subject: [PATCH 24/43] feat(views/table): expand child notes by default --- apps/client/src/widgets/view_widgets/table_view/index.ts | 1 + 1 file changed, 1 insertion(+) 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 6f7d49156..e03c7628b 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 = { ...opts, dataTree: hasChildren, + dataTreeStartExpanded: true, dataTreeElementColumn: "title", dataTreeExpandElement: ``, dataTreeCollapseElement: `` From 504a842d371673597e1db551df2850136fde71d1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 17:02:07 +0300 Subject: [PATCH 25/43] feat(views/table): force a refresh if #sorted is changed --- apps/client/src/widgets/view_widgets/table_view/index.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 e03c7628b..031c7dcc5 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -248,6 +248,11 @@ export default class TableView extends ViewMode { return; } + // Force a refresh if sorted is changed since we need to disable reordering. + if (loadResults.getAttributeRows().find(a => attributes.isAffecting(a, this.parentNote))) { + return true; + } + // Refresh if promoted attributes get changed. if (loadResults.getAttributeRows().find(attr => attr.type === "label" && From d4a4f15416ac6e881e2dffc95ef1f6c4c3b5425f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 17:27:38 +0300 Subject: [PATCH 26/43] refactor(views/table): move attribute detail widget to view --- .../attribute_widgets/attribute_detail.ts | 18 +++++++------ apps/client/src/widgets/note_list.ts | 25 +------------------ .../widgets/view_widgets/table_view/index.ts | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 9017673f3..9b66aa6f9 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -295,6 +295,7 @@ interface AttributeDetailOpts { x: number; y: number; focus?: "name"; + parent?: HTMLElement; } interface SearchRelatedResponse { @@ -560,19 +561,22 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.toggleInt(true); - const offset = this.parent?.$widget.offset() || { top: 0, left: 0 }; + const offset = this.parent?.$widget?.offset() || { top: 0, left: 0 }; const detPosition = this.getDetailPosition(x, offset); const outerHeight = this.$widget.outerHeight(); const height = $(window).height(); - if (detPosition && outerHeight && height) { - this.$widget - .css("left", detPosition.left) - .css("right", detPosition.right) - .css("top", y - offset.top + 70) - .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); + if (!detPosition || !outerHeight || !height) { + console.warn("Can't position popup, is it attached?"); + return; } + this.$widget + .css("left", detPosition.left) + .css("right", detPosition.right) + .css("top", y - offset.top + 70) + .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); + if (focus === "name") { this.$inputName.trigger("focus").trigger("select"); } diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index 1b00bd8c2..74602c921 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -3,8 +3,6 @@ import NoteListRenderer from "../services/note_list_renderer.js"; import type FNote from "../entities/fnote.js"; import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js"; import type ViewMode from "./view_widgets/view_mode.js"; -import AttributeDetailWidget from "./attribute_widgets/attribute_detail.js"; -import { Attribute } from "../services/attribute_parser.js"; const TPL = /*html*/`
@@ -39,7 +37,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { private noteIdRefreshed?: string; private shownNoteId?: string | null; private viewMode?: ViewMode | null; - private attributeDetailWidget: AttributeDetailWidget; private displayOnlyCollections: boolean; /** @@ -47,9 +44,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { */ constructor(displayOnlyCollections: boolean) { super(); - this.attributeDetailWidget = new AttributeDetailWidget() - .contentSized() - .setParent(this); + this.displayOnlyCollections = displayOnlyCollections; } @@ -72,7 +67,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { this.$widget = $(TPL); this.contentSized(); this.$content = this.$widget.find(".note-list-widget-content"); - this.$widget.append(this.attributeDetailWidget.render()); const observer = new IntersectionObserver( (entries) => { @@ -91,23 +85,6 @@ export default class NoteListWidget extends NoteContextAwareWidget { setTimeout(() => observer.observe(this.$widget[0]), 10); } - addNoteListItemEvent() { - const attr: Attribute = { - type: "label", - name: "label:myLabel", - value: "promoted,single,text" - }; - - this.attributeDetailWidget!.showAttributeDetail({ - attribute: attr, - allAttributes: [ attr ], - isOwned: true, - x: 100, - y: 200, - focus: "name" - }); - } - checkRenderStatus() { // console.log("this.isIntersecting", this.isIntersecting); // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId); 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 031c7dcc5..a56039cbe 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -14,6 +14,7 @@ import buildFooter from "./footer.js"; import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows.js"; import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail.js"; const TPL = /*html*/`
@@ -105,6 +106,8 @@ export default class TableView extends ViewMode { private api?: Tabulator; private newAttribute?: Attribute; private persistentData: StateInfo["tableData"]; + private attributeDetailWidget: AttributeDetailWidget; + constructor(args: ViewModeArgs) { super(args, "table"); @@ -114,6 +117,11 @@ export default class TableView extends ViewMode { this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); this.persistentData = {}; args.$parent.append(this.$root); + + this.attributeDetailWidget = new AttributeDetailWidget() + .contentSized() + .setParent(glob.getComponentByEl(args.$parent[0])); + args.$parent.append(this.attributeDetailWidget.render()); } async renderList() { @@ -226,6 +234,23 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } + addNoteListItemEvent() { + const attr: Attribute = { + type: "label", + name: "label:myLabel", + value: "promoted,single,text" + }; + + this.attributeDetailWidget!.showAttributeDetail({ + attribute: attr, + allAttributes: [ attr ], + isOwned: true, + x: 0, + y: 75, + focus: "name" + }); + } + addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { const parentNotePath = customNotePath ?? this.args.parentNotePath; if (parentNotePath) { From 2d4ac93221051a54b10ff7fb6ac60665247e15a0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 19:14:10 +0300 Subject: [PATCH 27/43] feat(views/table): basic implementation for inserting columns at position --- .../view_widgets/table_view/columns.ts | 31 ++++++++++++++----- .../view_widgets/table_view/context_menu.ts | 10 ++++++ .../widgets/view_widgets/table_view/index.ts | 19 +++++++++--- 3 files changed, 47 insertions(+), 13 deletions(-) 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 db42425c5..ad1fa334f 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -41,8 +41,8 @@ const labelTypeMappings: Record> = { } }; -export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[]) { - const columnDefs: ColumnDefinition[] = [ +export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[], position?: number) { + let columnDefs: ColumnDefinition[] = [ { title: "#", headerSort: false, @@ -86,25 +86,40 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m } if (existingColumnData) { - restoreExistingData(columnDefs, existingColumnData); + columnDefs = restoreExistingData(columnDefs, existingColumnData, position); } return columnDefs; } -function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) { +function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { const byField = new Map; for (const def of oldDefs) { byField.set(def.field ?? "", def); } + const newColumns: ColumnDefinition[] = []; + const existingColumns: ColumnDefinition[] = [] for (const newDef of newDefs) { const oldDef = byField.get(newDef.field ?? ""); if (!oldDef) { - continue; + newColumns.push(newDef); + } else { + newDef.width = oldDef.width; + newDef.visible = oldDef.visible; + existingColumns.push(newDef); } - - newDef.width = oldDef.width; - newDef.visible = oldDef.visible; } + + // Clamp position to a valid range + const insertPos = position !== undefined + ? Math.min(Math.max(position, 0), existingColumns.length) + : existingColumns.length; + + // Insert new columns at the specified position + return [ + ...existingColumns.slice(0, insertPos), + ...newColumns, + ...existingColumns.slice(insertPos) + ]; } 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 dd229ae87..005d76fc4 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 @@ -78,6 +78,16 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: uiIcon: "bx bx-empty", items: buildColumnItems() }, + { title: "----" }, + { + title: "Add column to the left", + handler: () => { + getParentComponent(e)?.triggerCommand("addNoteListItem", { + referenceColumn: column + }); + console.log("Add col"); + } + } ], selectMenuItemHandler() {}, x: e.pageX, 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 a56039cbe..ed4e03485 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -6,7 +6,7 @@ import SpacedUpdate from "../../../services/spaced_update.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; @@ -105,10 +105,10 @@ export default class TableView extends ViewMode { private spacedUpdate: SpacedUpdate; private api?: Tabulator; private newAttribute?: Attribute; + private newAttributePosition?: number; private persistentData: StateInfo["tableData"]; private attributeDetailWidget: AttributeDetailWidget; - constructor(args: ViewModeArgs) { super(args, "table"); @@ -234,13 +234,20 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } - addNoteListItemEvent() { + addNoteListItemEvent({ referenceColumn }: { referenceColumn?: ColumnComponent }) { + console.log("Add note list item ", referenceColumn); const attr: Attribute = { type: "label", name: "label:myLabel", value: "promoted,single,text" }; + if (referenceColumn && this.api) { + this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn) - 1; + } else { + this.newAttributePosition = undefined; + } + this.attributeDetailWidget!.showAttributeDetail({ attribute: attr, allAttributes: [ attr ], @@ -274,7 +281,7 @@ export default class TableView extends ViewMode { } // Force a refresh if sorted is changed since we need to disable reordering. - if (loadResults.getAttributeRows().find(a => attributes.isAffecting(a, this.parentNote))) { + if (loadResults.getAttributeRows().find(a => a.name === "sorted" && attributes.isAffecting(a, this.parentNote))) { return true; } @@ -283,6 +290,7 @@ export default class TableView extends ViewMode { attr.type === "label" && (attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) && attributes.isAffecting(attr, this.parentNote))) { + console.log("Col update"); this.#manageColumnUpdate(); } @@ -301,8 +309,9 @@ export default class TableView extends ViewMode { } const info = getAttributeDefinitionInformation(this.parentNote); - const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns); + const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.newAttributePosition); this.api.setColumns(columnDefs); + this.newAttributePosition = undefined; } async #manageRowsUpdate() { From 960d321019cbdd183b558c2a36ac63fbfd721dc9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 20:32:24 +0300 Subject: [PATCH 28/43] fix(views/table): position not restored after new columns (closes #6285) --- .../view_widgets/table_view/columns.spec.ts | 49 +++++++++++++++++++ .../view_widgets/table_view/columns.ts | 6 +-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/columns.spec.ts 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 new file mode 100644 index 000000000..64d5bc4b3 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/columns.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { restoreExistingData } from "./columns"; +import type { ColumnDefinition } from "tabulator-tables"; + +describe("restoreExistingData", () => { + it("should restore existing column data", () => { + const newDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", editor: "input" }, + { field: "noteId", title: "Note ID", visible: false } + ]; + const oldDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", width: 300, visible: true }, + { field: "noteId", title: "Note ID", width: 200, visible: true } + ]; + const restored = restoreExistingData(newDefs, oldDefs); + expect(restored[0].width).toBe(300); + expect(restored[1].width).toBe(200); + }); + + it("restores order of columns", () => { + const newDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", editor: "input" }, + { field: "noteId", title: "Note ID", visible: false } + ]; + const oldDefs: ColumnDefinition[] = [ + { field: "noteId", title: "Note ID", width: 200, visible: true }, + { field: "title", title: "Title", width: 300, visible: true } + ]; + const restored = restoreExistingData(newDefs, oldDefs); + expect(restored[0].field).toBe("noteId"); + expect(restored[1].field).toBe("title"); + }); + + it("inserts new columns at given position", () => { + const newDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", editor: "input" }, + { field: "noteId", title: "Note ID", visible: false }, + { field: "newColumn", title: "New Column", editor: "input" } + ]; + const oldDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", width: 300, visible: true }, + { field: "noteId", title: "Note ID", width: 200, visible: true } + ]; + const restored = restoreExistingData(newDefs, oldDefs, 0); + expect(restored[0].field).toBe("newColumn"); + expect(restored[1].field).toBe("title"); + expect(restored[2].field).toBe("noteId"); + }); +}); 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 ad1fa334f..d70cd8aca 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -92,14 +92,15 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m return columnDefs; } -function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { +export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { + const existingColumns: ColumnDefinition[] = [] const byField = new Map; for (const def of oldDefs) { byField.set(def.field ?? "", def); + existingColumns.push(def); } const newColumns: ColumnDefinition[] = []; - const existingColumns: ColumnDefinition[] = [] for (const newDef of newDefs) { const oldDef = byField.get(newDef.field ?? ""); if (!oldDef) { @@ -107,7 +108,6 @@ function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinit } else { newDef.width = oldDef.width; newDef.visible = oldDef.visible; - existingColumns.push(newDef); } } From e3d306cac3fd59d907904d29aebb192392e05d1a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 20:34:05 +0300 Subject: [PATCH 29/43] fix(views/table): wrong insert position for insert left --- apps/client/src/widgets/view_widgets/table_view/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 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 ed4e03485..5236f5621 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -235,7 +235,6 @@ export default class TableView extends ViewMode { } addNoteListItemEvent({ referenceColumn }: { referenceColumn?: ColumnComponent }) { - console.log("Add note list item ", referenceColumn); const attr: Attribute = { type: "label", name: "label:myLabel", @@ -243,7 +242,7 @@ export default class TableView extends ViewMode { }; if (referenceColumn && this.api) { - this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn) - 1; + this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); } else { this.newAttributePosition = undefined; } From cf31367acdd62f732b762eb12756f8e1368fdc29 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 20:42:37 +0300 Subject: [PATCH 30/43] feat(views/table): insert column to the right --- apps/client/src/components/app_context.ts | 5 +++++ .../widgets/view_widgets/table_view/context_menu.ts | 10 ++++++++++ .../src/widgets/view_widgets/table_view/index.ts | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index bbd124864..22dba15b3 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -29,6 +29,7 @@ import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; import { StartupChecks } from "./startup_checks.js"; import type { CreateNoteOpts } from "../services/note_create.js"; +import { ColumnComponent } from "tabulator-tables"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; @@ -282,6 +283,10 @@ export type CommandMappings = { customOpts: CreateNoteOpts; parentNotePath?: string; }; + addNoteListItem: CommandData & { + referenceColumn?: ColumnComponent; + direction?: "before" | "after"; + }; buildTouchBar: CommandData & { TouchBar: typeof TouchBar; 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 005d76fc4..ffee1cc8b 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 @@ -87,6 +87,16 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: }); console.log("Add col"); } + }, + { + title: "Add column to the right", + handler: () => { + getParentComponent(e)?.triggerCommand("addNoteListItem", { + referenceColumn: column, + direction: "after" + }); + console.log("Add col after"); + } } ], 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 5236f5621..c6881c1fa 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -234,7 +234,7 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } - addNoteListItemEvent({ referenceColumn }: { referenceColumn?: ColumnComponent }) { + addNoteListItemEvent({ referenceColumn, direction }: EventData<"addNoteListItem">) { const attr: Attribute = { type: "label", name: "label:myLabel", @@ -243,6 +243,10 @@ export default class TableView extends ViewMode { if (referenceColumn && this.api) { this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); + + if (direction === "after") { + this.newAttributePosition++; + } } else { this.newAttributePosition = undefined; } From ab093ed9a0ed1b00f41ed3b6aded465390447257 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 20:59:29 +0300 Subject: [PATCH 31/43] chore(views/table): add translations --- .../src/translations/en/translation.json | 4 ++- .../view_widgets/table_view/context_menu.ts | 26 ++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 4cb8bbefe..fae8d04a1 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1953,7 +1953,9 @@ "show-hide-columns": "Show/hide columns", "row-insert-above": "Insert row above", "row-insert-below": "Insert row below", - "row-insert-child": "Insert child note" + "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" }, "book_properties_config": { "hide-weekends": "Hide weekends", 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 ffee1cc8b..acfbc4ed7 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 @@ -80,23 +80,19 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: }, { title: "----" }, { - title: "Add column to the left", - handler: () => { - getParentComponent(e)?.triggerCommand("addNoteListItem", { - referenceColumn: column - }); - console.log("Add col"); - } + title: t("table_view.add-column-to-the-left"), + uiIcon: "bx bx-horizontal-left", + handler: () => getParentComponent(e)?.triggerCommand("addNoteListItem", { + referenceColumn: column + }) }, { - title: "Add column to the right", - handler: () => { - getParentComponent(e)?.triggerCommand("addNoteListItem", { - referenceColumn: column, - direction: "after" - }); - console.log("Add col after"); - } + title: t("table_view.add-column-to-the-right"), + uiIcon: "bx bx-horizontal-right", + handler: () => getParentComponent(e)?.triggerCommand("addNoteListItem", { + referenceColumn: column, + direction: "after" + }) } ], selectMenuItemHandler() {}, From c9b37dcc775e6fc8e1a8a287ede4b94671c5fefa Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 21:06:44 +0300 Subject: [PATCH 32/43] refactor(views/table): rename event --- apps/client/src/components/app_context.ts | 2 +- .../src/widgets/view_widgets/table_view/context_menu.ts | 4 ++-- apps/client/src/widgets/view_widgets/table_view/footer.ts | 2 +- apps/client/src/widgets/view_widgets/table_view/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 22dba15b3..51f8120a9 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -283,7 +283,7 @@ export type CommandMappings = { customOpts: CreateNoteOpts; parentNotePath?: string; }; - addNoteListItem: CommandData & { + addNewTableColumn: CommandData & { referenceColumn?: ColumnComponent; direction?: "before" | "after"; }; 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 acfbc4ed7..816e5c27c 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 @@ -82,14 +82,14 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: { title: t("table_view.add-column-to-the-left"), uiIcon: "bx bx-horizontal-left", - handler: () => getParentComponent(e)?.triggerCommand("addNoteListItem", { + handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn: column }) }, { title: t("table_view.add-column-to-the-right"), uiIcon: "bx bx-horizontal-right", - handler: () => getParentComponent(e)?.triggerCommand("addNoteListItem", { + handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn: column, direction: "after" }) diff --git a/apps/client/src/widgets/view_widgets/table_view/footer.ts b/apps/client/src/widgets/view_widgets/table_view/footer.ts index fc533d6a7..64a440236 100644 --- a/apps/client/src/widgets/view_widgets/table_view/footer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/footer.ts @@ -15,7 +15,7 @@ export default function buildFooter(parentNote: FNote) { ${t("table_view.new-row")} - `.trimStart(); 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 c6881c1fa..729091d95 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -234,7 +234,7 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } - addNoteListItemEvent({ referenceColumn, direction }: EventData<"addNoteListItem">) { + addNewTableColumnEvent({ referenceColumn, direction }: EventData<"addNewTableColumn">) { const attr: Attribute = { type: "label", name: "label:myLabel", From b1f0c64ef23a29f5e8cd9b363b01cd295b79e706 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 22:52:37 +0300 Subject: [PATCH 33/43] chore(views/geo): typing issue --- apps/client/src/widgets/view_widgets/geo_view/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/view_widgets/geo_view/index.ts b/apps/client/src/widgets/view_widgets/geo_view/index.ts index 2ead57bde..0ea27e61b 100644 --- a/apps/client/src/widgets/view_widgets/geo_view/index.ts +++ b/apps/client/src/widgets/view_widgets/geo_view/index.ts @@ -251,7 +251,7 @@ export default class GeoView extends ViewMode { } } - async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { // If any of the children branches are altered. if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) { this.#reloadMarkers(); From 4321c161ac7d38439215577ea8cc623a8df4427c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 23:07:26 +0300 Subject: [PATCH 34/43] fix(views/calendar): duplicate entries in calendar view --- apps/client/src/widgets/view_widgets/calendar_view.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index 8402d1eb8..b3f3316d9 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -458,13 +458,6 @@ export default class CalendarView extends ViewMode<{}> { for (const note of notes) { const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate"); - if (note.hasChildren()) { - const childrenEventData = await this.buildEvents(note.getChildNoteIds()); - if (childrenEventData.length > 0) { - events.push(childrenEventData); - } - } - if (!startDate) { continue; } From df2cede07560031810846479544ee173e6149fb3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 23:12:55 +0300 Subject: [PATCH 35/43] fix(views/calendar): nested entries in calendar view --- apps/client/src/widgets/view_widgets/calendar_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index b3f3316d9..e4cb03f17 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -432,7 +432,7 @@ export default class CalendarView extends ViewMode<{}> { events.push(await CalendarView.buildEvent(dateNote, { startDate })); if (dateNote.hasChildren()) { - const childNoteIds = dateNote.getChildNoteIds(); + const childNoteIds = await dateNote.getSubtreeNoteIds(); for (const childNoteId of childNoteIds) { childNoteToDateMapping[childNoteId] = startDate; } From 3f5df18d6c0bb65b4b331dd1b8fc997c95499249 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Mon, 14 Jul 2025 21:12:00 +0000 Subject: [PATCH 36/43] fix(api): also rate limit etapi docs endpoint --- apps/server/src/etapi/spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/server/src/etapi/spec.ts b/apps/server/src/etapi/spec.ts index 7ef963f8f..04b814c8c 100644 --- a/apps/server/src/etapi/spec.ts +++ b/apps/server/src/etapi/spec.ts @@ -3,12 +3,18 @@ import type { Router } from "express"; import fs from "fs"; import path from "path"; import { RESOURCE_DIR } from "../services/resource_dir"; +import rateLimit from "express-rate-limit"; const specPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml"); let spec: string | null = null; +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs +}); + function register(router: Router) { - router.get("/etapi/etapi.openapi.yaml", (_, res) => { + router.get("/etapi/etapi.openapi.yaml", limiter, (_, res) => { if (!spec) { spec = fs.readFileSync(specPath, "utf8"); } From 5a7a0d32d161595b7ba8302cba534c3c3db0df2b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 14:53:18 +0300 Subject: [PATCH 37/43] refactor(views/table): move col editing to own component --- .../view_widgets/table_view/col_editing.ts | 80 +++++++++++++++++++ .../widgets/view_widgets/table_view/index.ts | 67 +++------------- 2 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/col_editing.ts 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 new file mode 100644 index 000000000..3d44660ae --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts @@ -0,0 +1,80 @@ +import { Tabulator } from "tabulator-tables"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import { Attribute } from "../../../services/attribute_parser"; +import Component from "../../../components/component"; +import { CommandListenerData, EventData } from "../../../components/app_context"; +import attributes from "../../../services/attributes"; +import FNote from "../../../entities/fnote"; + +export default class TableColumnEditing extends Component { + + private attributeDetailWidget: AttributeDetailWidget; + private newAttributePosition?: number; + private api: Tabulator; + private newAttribute?: Attribute; + private parentNote: FNote; + + constructor($parent: JQuery, parentNote: FNote, api: Tabulator) { + super(); + const parentComponent = glob.getComponentByEl($parent[0]); + this.attributeDetailWidget = new AttributeDetailWidget() + .contentSized() + .setParent(parentComponent); + $parent.append(this.attributeDetailWidget.render()); + this.api = api; + this.parentNote = parentNote; + } + + addNewTableColumnEvent({ referenceColumn, direction }: EventData<"addNewTableColumn">) { + const attr: Attribute = { + type: "label", + name: "label:myLabel", + value: "promoted,single,text" + }; + + if (referenceColumn && this.api) { + this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); + + if (direction === "after") { + this.newAttributePosition++; + } + } else { + this.newAttributePosition = undefined; + } + + this.attributeDetailWidget!.showAttributeDetail({ + attribute: attr, + allAttributes: [ attr ], + isOwned: true, + x: 0, + y: 75, + focus: "name" + }); + } + + async reloadAttributesEvent() { + console.log("Reload attributes"); + } + + async updateAttributeListEvent({ attributes }: CommandListenerData<"updateAttributeList">) { + this.newAttribute = attributes[0]; + } + + async saveAttributesEvent() { + if (!this.newAttribute) { + return; + } + + const { name, value } = this.newAttribute; + attributes.addLabel(this.parentNote.noteId, name, value, true); + } + + getNewAttributePosition() { + return this.newAttributePosition; + } + + resetNewAttributePosition() { + this.newAttributePosition = 0; + } + +} 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 729091d95..94284dc74 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -4,17 +4,16 @@ import attributes, { setAttribute, setLabel } from "../../../services/attributes import server from "../../../services/server.js"; import SpacedUpdate from "../../../services/spaced_update.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; -import type { Attribute } from "../../../services/attribute_parser.js"; import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; import buildFooter from "./footer.js"; -import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows.js"; +import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js"; import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; -import AttributeDetailWidget from "../../attribute_widgets/attribute_detail.js"; +import TableColumnEditing from "./col_editing.js"; const TPL = /*html*/`
@@ -104,10 +103,8 @@ export default class TableView extends ViewMode { private $container: JQuery; private spacedUpdate: SpacedUpdate; private api?: Tabulator; - private newAttribute?: Attribute; - private newAttributePosition?: number; private persistentData: StateInfo["tableData"]; - private attributeDetailWidget: AttributeDetailWidget; + private colEditing?: TableColumnEditing; constructor(args: ViewModeArgs) { super(args, "table"); @@ -117,11 +114,6 @@ export default class TableView extends ViewMode { this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); this.persistentData = {}; args.$parent.append(this.$root); - - this.attributeDetailWidget = new AttributeDetailWidget() - .contentSized() - .setParent(glob.getComponentByEl(args.$parent[0])); - args.$parent.append(this.attributeDetailWidget.render()); } async renderList() { @@ -175,6 +167,10 @@ export default class TableView extends ViewMode { } this.api = new Tabulator(el, opts); + + this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api); + this.child(this.colEditing); + if (movableRows) { configureReorderingRows(this.api); } @@ -216,51 +212,6 @@ export default class TableView extends ViewMode { }); } - async reloadAttributesCommand() { - console.log("Reload attributes"); - } - - async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { - this.newAttribute = attributes[0]; - } - - async saveAttributesCommand() { - if (!this.newAttribute) { - return; - } - - const { name, value } = this.newAttribute; - attributes.addLabel(this.parentNote.noteId, name, value, true); - console.log("Save attributes", this.newAttribute); - } - - addNewTableColumnEvent({ referenceColumn, direction }: EventData<"addNewTableColumn">) { - const attr: Attribute = { - type: "label", - name: "label:myLabel", - value: "promoted,single,text" - }; - - if (referenceColumn && this.api) { - this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); - - if (direction === "after") { - this.newAttributePosition++; - } - } else { - this.newAttributePosition = undefined; - } - - this.attributeDetailWidget!.showAttributeDetail({ - attribute: attr, - allAttributes: [ attr ], - isOwned: true, - x: 0, - y: 75, - focus: "name" - }); - } - addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { const parentNotePath = customNotePath ?? this.args.parentNotePath; if (parentNotePath) { @@ -312,9 +263,9 @@ export default class TableView extends ViewMode { } const info = getAttributeDefinitionInformation(this.parentNote); - const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.newAttributePosition); + const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.colEditing?.getNewAttributePosition()); this.api.setColumns(columnDefs); - this.newAttributePosition = undefined; + this.colEditing?.resetNewAttributePosition(); } async #manageRowsUpdate() { From b91a3e13b065ba486438030c0b3cef426d65385a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 15:04:41 +0300 Subject: [PATCH 38/43] refactor(views/table): move row editing to own component --- .../widgets/view_widgets/table_view/index.ts | 91 +---------------- .../view_widgets/table_view/row_editing.ts | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+), 86 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/row_editing.ts 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 94284dc74..daabc898e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,10 +1,7 @@ -import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; -import attributes, { setAttribute, setLabel } from "../../../services/attributes.js"; -import server from "../../../services/server.js"; +import attributes from "../../../services/attributes.js"; import SpacedUpdate from "../../../services/spaced_update.js"; -import type { CommandListenerData, EventData } from "../../../components/app_context.js"; -import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; +import type { EventData } from "../../../components/app_context.js"; import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import "../../../../src/stylesheets/table.css"; @@ -14,6 +11,7 @@ import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.j import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js"; import { setupContextMenu } from "./context_menu.js"; import TableColumnEditing from "./col_editing.js"; +import TableRowEditing from "./row_editing.js"; const TPL = /*html*/`
@@ -171,11 +169,12 @@ export default class TableView extends ViewMode { this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api); this.child(this.colEditing); + this.child(new TableRowEditing(this.api, this.args.parentNotePath!)); + if (movableRows) { configureReorderingRows(this.api); } setupContextMenu(this.api, this.parentNote); - this.setupEditing(); } private onSave() { @@ -184,51 +183,6 @@ export default class TableView extends ViewMode { }); } - private setupEditing() { - this.api!.on("cellEdited", async (cell) => { - const noteId = cell.getRow().getData().noteId; - const field = cell.getField(); - let newValue = cell.getValue(); - - if (field === "title") { - server.put(`notes/${noteId}/title`, { title: newValue }); - return; - } - - if (field.includes(".")) { - const [ type, name ] = field.split(".", 2); - if (type === "labels") { - if (typeof newValue === "boolean") { - newValue = newValue ? "true" : "false"; - } - setLabel(noteId, name, newValue); - } else if (type === "relations") { - const note = await froca.getNote(noteId); - if (note) { - setAttribute(note, "relation", name, newValue); - } - } - } - }); - } - - addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { - const parentNotePath = customNotePath ?? this.args.parentNotePath; - if (parentNotePath) { - const opts: CreateNoteOpts = { - activate: false, - ...customOpts - } - note_create.createNote(parentNotePath, opts).then(({ branch }) => { - if (branch) { - setTimeout(() => { - this.focusOnBranch(branch?.branchId); - }); - } - }) - } - } - async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) { if (!this.api) { return; @@ -285,40 +239,5 @@ export default class TableView extends ViewMode { return false; } - focusOnBranch(branchId: string) { - if (!this.api) { - return; - } - - const row = findRowDataById(this.api.getRows(), branchId); - if (!row) { - return; - } - - // Expand the parent tree if any. - if (this.api.options.dataTree) { - const parent = row.getTreeParent(); - if (parent) { - parent.treeExpand(); - } - } - - row.getCell("title").edit(); - } - } - -function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { - for (let row of rows) { - const item = row.getIndex() as string; - - if (item === branchId) { - return row; - } - - let found = findRowDataById(row.getTreeChildren(), branchId); - if (found) return found; - } - return null; -} diff --git a/apps/client/src/widgets/view_widgets/table_view/row_editing.ts b/apps/client/src/widgets/view_widgets/table_view/row_editing.ts new file mode 100644 index 000000000..b1515034c --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/row_editing.ts @@ -0,0 +1,97 @@ +import { RowComponent, Tabulator } from "tabulator-tables"; +import Component from "../../../components/component.js"; +import { setAttribute, setLabel } from "../../../services/attributes.js"; +import server from "../../../services/server.js"; +import froca from "../../../services/froca.js"; +import note_create, { CreateNoteOpts } from "../../../services/note_create.js"; +import { CommandListenerData } from "../../../components/app_context.js"; + +export default class TableRowEditing extends Component { + + private parentNotePath: string; + private api: Tabulator; + + constructor(api: Tabulator, parentNotePath: string) { + super(); + this.api = api; + this.parentNotePath = parentNotePath; + api.on("cellEdited", async (cell) => { + const noteId = cell.getRow().getData().noteId; + const field = cell.getField(); + let newValue = cell.getValue(); + + if (field === "title") { + server.put(`notes/${noteId}/title`, { title: newValue }); + return; + } + + if (field.includes(".")) { + const [ type, name ] = field.split(".", 2); + if (type === "labels") { + if (typeof newValue === "boolean") { + newValue = newValue ? "true" : "false"; + } + setLabel(noteId, name, newValue); + } else if (type === "relations") { + const note = await froca.getNote(noteId); + if (note) { + setAttribute(note, "relation", name, newValue); + } + } + } + }); + } + + addNewRowEvent({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { + const parentNotePath = customNotePath ?? this.parentNotePath; + if (parentNotePath) { + const opts: CreateNoteOpts = { + activate: false, + ...customOpts + } + note_create.createNote(parentNotePath, opts).then(({ branch }) => { + if (branch) { + setTimeout(() => { + this.focusOnBranch(branch?.branchId); + }); + } + }) + } + } + + focusOnBranch(branchId: string) { + if (!this.api) { + return; + } + + const row = findRowDataById(this.api.getRows(), branchId); + if (!row) { + return; + } + + // Expand the parent tree if any. + if (this.api.options.dataTree) { + const parent = row.getTreeParent(); + if (parent) { + parent.treeExpand(); + } + } + + row.getCell("title").edit(); + } + +} + +function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { + for (let row of rows) { + const item = row.getIndex() as string; + + if (item === branchId) { + return row; + } + + let found = findRowDataById(row.getTreeChildren(), branchId); + if (found) return found; + } + return null; +} From 8131a4b3d2f500263c13cb22eff9db2d04e29423 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 15:49:32 +0300 Subject: [PATCH 39/43] fix(views/table): events/commands not well sent --- .../view_widgets/table_view/col_editing.ts | 10 +++------ .../widgets/view_widgets/table_view/index.ts | 22 ++++++++++++++++--- .../view_widgets/table_view/row_editing.ts | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) 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 3d44660ae..193714a6e 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 @@ -25,7 +25,7 @@ export default class TableColumnEditing extends Component { this.parentNote = parentNote; } - addNewTableColumnEvent({ referenceColumn, direction }: EventData<"addNewTableColumn">) { + addNewTableColumnCommand({ referenceColumn, direction }: EventData<"addNewTableColumn">) { const attr: Attribute = { type: "label", name: "label:myLabel", @@ -52,15 +52,11 @@ export default class TableColumnEditing extends Component { }); } - async reloadAttributesEvent() { - console.log("Reload attributes"); - } - - async updateAttributeListEvent({ attributes }: CommandListenerData<"updateAttributeList">) { + async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { this.newAttribute = attributes[0]; } - async saveAttributesEvent() { + async saveAttributesCommand() { if (!this.newAttribute) { return; } 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 daabc898e..2d3a9b928 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -103,6 +103,7 @@ export default class TableView extends ViewMode { private api?: Tabulator; private persistentData: StateInfo["tableData"]; private colEditing?: TableColumnEditing; + private rowEditing?: TableRowEditing; constructor(args: ViewModeArgs) { super(args, "table"); @@ -167,9 +168,7 @@ export default class TableView extends ViewMode { this.api = new Tabulator(el, opts); this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api); - this.child(this.colEditing); - - this.child(new TableRowEditing(this.api, this.args.parentNotePath!)); + this.rowEditing = new TableRowEditing(this.api, this.args.parentNotePath!); if (movableRows) { configureReorderingRows(this.api); @@ -222,6 +221,23 @@ 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(); + } + + async #manageRowsUpdate() { if (!this.api) { return; diff --git a/apps/client/src/widgets/view_widgets/table_view/row_editing.ts b/apps/client/src/widgets/view_widgets/table_view/row_editing.ts index b1515034c..92b0eeea4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/row_editing.ts +++ b/apps/client/src/widgets/view_widgets/table_view/row_editing.ts @@ -42,7 +42,7 @@ export default class TableRowEditing extends Component { }); } - addNewRowEvent({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { + addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { const parentNotePath = customNotePath ?? this.parentNotePath; if (parentNotePath) { const opts: CreateNoteOpts = { From a04804d3fa4b276e31325bf4ee3c5ccbd6a9216d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 18:39:20 +0300 Subject: [PATCH 40/43] fix(views/table): wrong specs when restoring columns --- .../view_widgets/table_view/columns.spec.ts | 32 +++++++++++++++++ .../view_widgets/table_view/columns.ts | 34 +++++++++---------- 2 files changed, 49 insertions(+), 17 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 64d5bc4b3..083d03820 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 @@ -3,6 +3,20 @@ import { restoreExistingData } from "./columns"; import type { ColumnDefinition } from "tabulator-tables"; describe("restoreExistingData", () => { + it("maintains important columns properties", () => { + const newDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", editor: "input" }, + { field: "noteId", title: "Note ID", formatter: "color", visible: false } + ]; + const oldDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", width: 300, visible: true }, + { field: "noteId", title: "Note ID", width: 200, visible: true } + ]; + const restored = restoreExistingData(newDefs, oldDefs); + expect(restored[0].editor).toBe("input"); + expect(restored[1].formatter).toBe("color"); + }); + it("should restore existing column data", () => { const newDefs: ColumnDefinition[] = [ { field: "title", title: "Title", editor: "input" }, @@ -42,8 +56,26 @@ describe("restoreExistingData", () => { { field: "noteId", title: "Note ID", width: 200, visible: true } ]; const restored = restoreExistingData(newDefs, oldDefs, 0); + expect(restored.length).toBe(3); expect(restored[0].field).toBe("newColumn"); expect(restored[1].field).toBe("title"); expect(restored[2].field).toBe("noteId"); }); + + it("inserts new columns at the end if no position is specified", () => { + const newDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", editor: "input" }, + { field: "noteId", title: "Note ID", visible: false }, + { field: "newColumn", title: "New Column", editor: "input" } + ]; + const oldDefs: ColumnDefinition[] = [ + { field: "title", title: "Title", width: 300, visible: true }, + { field: "noteId", title: "Note ID", width: 200, visible: true } + ]; + const restored = restoreExistingData(newDefs, oldDefs); + expect(restored.length).toBe(3); + expect(restored[0].field).toBe("title"); + expect(restored[1].field).toBe("noteId"); + expect(restored[2].field).toBe("newColumn"); + }); }); 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 d70cd8aca..560efdf4b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -93,30 +93,30 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m } export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) { - const existingColumns: ColumnDefinition[] = [] - const byField = new Map; - for (const def of oldDefs) { - byField.set(def.field ?? "", def); - existingColumns.push(def); - } + // 1. Keep existing columns, but restore their properties like width, visibility and order. + const newItemsByField = new Map( + newDefs.map(def => [def.field!, def]) + ); + const existingColumns = oldDefs + .map(item => { + return { + ...newItemsByField.get(item.field!), + width: item.width, + visible: item.visible, + }; + }) as ColumnDefinition[]; - const newColumns: ColumnDefinition[] = []; - for (const newDef of newDefs) { - const oldDef = byField.get(newDef.field ?? ""); - if (!oldDef) { - newColumns.push(newDef); - } else { - newDef.width = oldDef.width; - newDef.visible = oldDef.visible; - } - } + // 2. Determine new columns. + const existingFields = new Set(existingColumns.map(item => item.field)); + const newColumns = newDefs + .filter(item => !existingFields.has(item.field!)); // Clamp position to a valid range const insertPos = position !== undefined ? Math.min(Math.max(position, 0), existingColumns.length) : existingColumns.length; - // Insert new columns at the specified position + // 3. Insert new columns at the specified position return [ ...existingColumns.slice(0, insertPos), ...newColumns, From 7cd0e664ac06b53618b556d9a422985e91a31a5e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 18:51:51 +0300 Subject: [PATCH 41/43] feat(views/table): basic editing of columns (rename not supported) --- apps/client/src/components/app_context.ts | 1 + .../src/translations/en/translation.json | 3 +- .../view_widgets/table_view/col_editing.ts | 44 +++++++++++++++---- .../view_widgets/table_view/context_menu.ts | 7 +++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 51f8120a9..24545fdaa 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -284,6 +284,7 @@ export type CommandMappings = { parentNotePath?: string; }; addNewTableColumn: CommandData & { + columnToEdit?: ColumnComponent; referenceColumn?: ColumnComponent; direction?: "before" | "after"; }; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index fae8d04a1..cfc80a537 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1955,7 +1955,8 @@ "row-insert-below": "Insert row below", "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" + "add-column-to-the-right": "Add column to the right", + "edit-column": "Edit column" }, "book_properties_config": { "hide-weekends": "Hide weekends", 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 193714a6e..983b89b1d 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 @@ -25,12 +25,21 @@ export default class TableColumnEditing extends Component { this.parentNote = parentNote; } - addNewTableColumnCommand({ referenceColumn, direction }: EventData<"addNewTableColumn">) { - const attr: Attribute = { - type: "label", - name: "label:myLabel", - value: "promoted,single,text" - }; + addNewTableColumnCommand({ referenceColumn, columnToEdit, direction }: EventData<"addNewTableColumn">) { + let attr: Attribute | undefined; + + if (columnToEdit) { + attr = this.getAttributeFromField(columnToEdit.getField()); + console.log("Built ", attr); + } + + if (!attr) { + attr = { + type: "label", + name: "label:myLabel", + value: "promoted,single,text" + }; + } if (referenceColumn && this.api) { this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn); @@ -47,7 +56,7 @@ export default class TableColumnEditing extends Component { allAttributes: [ attr ], isOwned: true, x: 0, - y: 75, + y: 150, focus: "name" }); } @@ -62,7 +71,7 @@ export default class TableColumnEditing extends Component { } const { name, value } = this.newAttribute; - attributes.addLabel(this.parentNote.noteId, name, value, true); + attributes.setLabel(this.parentNote.noteId, name, value); } getNewAttributePosition() { @@ -73,4 +82,23 @@ export default class TableColumnEditing extends Component { this.newAttributePosition = 0; } + getFAttributeFromField(field: string) { + const [ type, name ] = field.split(".", 2); + const attrName = `${type.replace("s", "")}:${name}`; + return this.parentNote.getLabel(attrName); + } + + getAttributeFromField(field: string) { + const fAttribute = this.getFAttributeFromField(field); + if (fAttribute) { + return { + name: fAttribute.name, + value: fAttribute.value, + type: fAttribute.type + }; + } + return undefined; + } + } + 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 816e5c27c..0c7ed4760 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 @@ -86,6 +86,13 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: referenceColumn: column }) }, + { + title: t("table_view.edit-column"), + uiIcon: "bx bx-edit", + handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", { + columnToEdit: column + }) + }, { title: t("table_view.add-column-to-the-right"), uiIcon: "bx bx-horizontal-right", From aa8902f5b94db213b2bef5bfd7d06c5e264f8c71 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Jul 2025 18:55:29 +0300 Subject: [PATCH 42/43] fix(client): popup not displayed for existing attributes (closes #5718) --- apps/client/src/widgets/attribute_widgets/attribute_editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts index 08bfefac3..3e97723a5 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts @@ -426,7 +426,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem curNode = curNode.previousSibling; if ((curNode as ModelElement).name === "reference") { - clickIndex += (curNode.getAttribute("notePath") as string).length + 1; + clickIndex += (curNode.getAttribute("href") as string).length + 1; } else if ("data" in curNode) { clickIndex += (curNode.data as string).length; } From cf8063f311664cd5c45b8dc345e120bdc92ae2d9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 14 Jul 2025 23:23:25 +0300 Subject: [PATCH 43/43] feat(views/table): format note ID as monospace --- apps/client/src/widgets/view_widgets/table_view/columns.ts | 5 +++-- .../client/src/widgets/view_widgets/table_view/formatters.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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 560efdf4b..7a8348e4e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/columns.ts +++ b/apps/client/src/widgets/view_widgets/table_view/columns.ts @@ -1,6 +1,6 @@ import { RelationEditor } from "./relation_editor.js"; -import { NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; -import type { ColumnDefinition, Tabulator } from "tabulator-tables"; +import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js"; +import type { ColumnDefinition } from "tabulator-tables"; import { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; type ColumnType = LabelType | "relation"; @@ -55,6 +55,7 @@ export function buildColumnDefinitions(info: AttributeDefinitionInformation[], m { field: "noteId", title: "Note ID", + formatter: MonospaceFormatter, visible: false }, { diff --git a/apps/client/src/widgets/view_widgets/table_view/formatters.ts b/apps/client/src/widgets/view_widgets/table_view/formatters.ts index 15701bc5c..07c9d1060 100644 --- a/apps/client/src/widgets/view_widgets/table_view/formatters.ts +++ b/apps/client/src/widgets/view_widgets/table_view/formatters.ts @@ -47,6 +47,10 @@ export function RowNumberFormatter(draggableRows: boolean) { }; } +export function MonospaceFormatter(cell: CellComponent) { + return `${cell.getValue()}`; +} + function buildNoteLink(noteId: string) { const $noteRef = $(""); const href = `#root/${noteId}`;