From fcd71957ffe6185630762e01bff5acc4188d9379 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 10:31:41 +0300 Subject: [PATCH 001/107] feat(book/table): create new view type --- .../client/src/services/note_list_renderer.ts | 24 ++++++++++++------- .../src/translations/en/translation.json | 3 ++- .../widgets/ribbon_widgets/book_properties.ts | 3 ++- .../src/widgets/view_widgets/table_view.ts | 24 +++++++++++++++++++ 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view.ts diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index ede0c3132..a55e93c8a 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -1,10 +1,11 @@ import type FNote from "../entities/fnote.js"; import CalendarView from "../widgets/view_widgets/calendar_view.js"; import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; +import TableView from "../widgets/view_widgets/table_view.js"; import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; import type ViewMode from "../widgets/view_widgets/view_mode.js"; -export type ViewTypeOptions = "list" | "grid" | "calendar"; +export type ViewTypeOptions = "list" | "grid" | "calendar" | "table"; export default class NoteListRenderer { @@ -20,19 +21,26 @@ export default class NoteListRenderer { showNotePath }; - if (this.viewType === "list" || this.viewType === "grid") { - this.viewMode = new ListOrGridView(this.viewType, args); - } else if (this.viewType === "calendar") { - this.viewMode = new CalendarView(args); - } else { - this.viewMode = null; + 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; + default: + this.viewMode = null; } } #getViewType(parentNote: FNote): ViewTypeOptions { const viewType = parentNote.getLabelValue("viewType"); - if (!["list", "grid", "calendar"].includes(viewType || "")) { + if (!["list", "grid", "calendar", "table"].includes(viewType || "")) { // when not explicitly set, decide based on the note type return parentNote.type === "search" ? "list" : "grid"; } else { diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index e3ae657b7..fadf09a4a 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -760,7 +760,8 @@ "expand": "Expand", "book_properties": "Book Properties", "invalid_view_type": "Invalid view type '{{type}}'", - "calendar": "Calendar" + "calendar": "Calendar", + "table": "Table" }, "edited_notes": { "no_edited_notes_found": "No edited notes on this day yet...", diff --git a/apps/client/src/widgets/ribbon_widgets/book_properties.ts b/apps/client/src/widgets/ribbon_widgets/book_properties.ts index f1a06f640..5be7a86c5 100644 --- a/apps/client/src/widgets/ribbon_widgets/book_properties.ts +++ b/apps/client/src/widgets/ribbon_widgets/book_properties.ts @@ -24,6 +24,7 @@ const TPL = /*html*/` + @@ -126,7 +127,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { return; } - if (!["list", "grid", "calendar"].includes(type)) { + if (!["list", "grid", "calendar", "table"].includes(type)) { throw new Error(t("book_properties.invalid_view_type", { type })); } diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts new file mode 100644 index 000000000..6029df9e6 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -0,0 +1,24 @@ +import ViewMode, { ViewModeArgs } from "./view_mode"; + +const TPL = /*html*/` +
+

Table view goes here.

+
+`; + +export default class TableView extends ViewMode { + + private $root: JQuery; + + constructor(args: ViewModeArgs) { + super(args); + + this.$root = $(TPL); + args.$parent.append(this.$root); + } + + async renderList() { + return this.$root; + } + +} From 5450bdeae9d4612ce102a749b5938991ef661caa Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 10:34:03 +0300 Subject: [PATCH 002/107] feat(book/table): hide no children warning --- apps/client/src/widgets/type_widgets/book.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/book.ts b/apps/client/src/widgets/type_widgets/book.ts index 9b11129bb..66d476464 100644 --- a/apps/client/src/widgets/type_widgets/book.ts +++ b/apps/client/src/widgets/type_widgets/book.ts @@ -36,7 +36,21 @@ export default class BookTypeWidget extends TypeWidget { } async doRefresh(note: FNote) { - this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar"); + this.$helpNoChildren.toggle(this.shouldDisplayNoChildrenWarning()); + } + + shouldDisplayNoChildrenWarning() { + if (this.note?.hasChildren()) { + return false; + } + + switch (this.note?.getAttributeValue("label", "viewType")) { + case "calendar": + case "table": + return false; + default: + return true; + } } entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { From a19186c50800f6d1e84c364c2fc249d565cb575d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 10:40:04 +0300 Subject: [PATCH 003/107] feat(book/table): set full height --- apps/client/src/widgets/view_widgets/table_view.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 6029df9e6..926e0c127 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -2,6 +2,16 @@ import ViewMode, { ViewModeArgs } from "./view_mode"; const TPL = /*html*/`
+ +

Table view goes here.

`; @@ -17,6 +27,10 @@ export default class TableView extends ViewMode { args.$parent.append(this.$root); } + get isFullHeight(): boolean { + return true; + } + async renderList() { return this.$root; } From 1b5dd4638dec32bd4ef3d5a6ae152301e4c08e51 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 10:40:11 +0300 Subject: [PATCH 004/107] chore(book/table): install ag-grid --- apps/client/package.json | 1 + pnpm-lock.yaml | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/apps/client/package.json b/apps/client/package.json index 77c7a559d..fccd3af97 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -26,6 +26,7 @@ "@triliumnext/commons": "workspace:*", "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", + "ag-grid-community": "33.3.2", "autocomplete.js": "0.38.1", "bootstrap": "5.3.7", "boxicons": "2.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8af72e4ee..20114062b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: '@triliumnext/share-theme': specifier: workspace:* version: link:../../packages/share-theme + ag-grid-community: + specifier: 33.3.2 + version: 33.3.2 autocomplete.js: specifier: 0.38.1 version: 0.38.1 @@ -5986,6 +5989,12 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + ag-charts-types@11.3.2: + resolution: {integrity: sha512-trPGqgGYiTeLgtf9nLuztDYOPOFOLbqHn1g2D99phf7QowcwdX0TPx0wfWG8Hm90LjB8IH+G2s3AZe2vrdAtMQ==} + + ag-grid-community@33.3.2: + resolution: {integrity: sha512-9bx0e/+ykOyLvUxHqmdy0cRVANH6JAtv0yZdnBZEXYYqBAwN+G5a4NY+2I1KvoOCYzbk8SnStG7y4hCdVAAWOQ==} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -21124,6 +21133,12 @@ snapshots: address@1.2.2: {} + ag-charts-types@11.3.2: {} + + ag-grid-community@33.3.2: + dependencies: + ag-charts-types: 11.3.2 + agent-base@6.0.2: dependencies: debug: 4.4.1(supports-color@6.0.0) From 894a26cc67bb0c3e564698d56aaa1641655a4b10 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 10:49:33 +0300 Subject: [PATCH 005/107] feat(book/table): set up sample grid --- .../src/widgets/view_widgets/table_view.ts | 13 +++++++++++- .../view_widgets/table_view/renderer.ts | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/renderer.ts diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 926e0c127..2f47f67e7 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -1,3 +1,4 @@ +import renderTable from "./table_view/renderer"; import ViewMode, { ViewModeArgs } from "./view_mode"; const TPL = /*html*/` @@ -10,20 +11,28 @@ const TPL = /*html*/` user-select: none; padding: 10px; } + + .table-view-container { + height: 100%; + } -

Table view goes here.

+
+

Table view goes here.

+
`; export default class TableView extends ViewMode { private $root: JQuery; + private $container: JQuery; constructor(args: ViewModeArgs) { super(args); this.$root = $(TPL); + this.$container = this.$root.find(".table-view-container"); args.$parent.append(this.$root); } @@ -32,6 +41,8 @@ export default class TableView extends ViewMode { } async renderList() { + this.$container.empty(); + renderTable(this.$container[0]); return this.$root; } diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts new file mode 100644 index 000000000..31559bc10 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -0,0 +1,21 @@ +import { createGrid, AllCommunityModule, ModuleRegistry } from "ag-grid-community"; + +ModuleRegistry.registerModules([ AllCommunityModule ]); + +export default function renderTable(el: HTMLElement) { + createGrid(el, { + // Row Data: The data to be displayed. + rowData: [ + { make: "Tesla", model: "Model Y", price: 64950, electric: true }, + { make: "Ford", model: "F-Series", price: 33850, electric: false }, + { make: "Toyota", model: "Corolla", price: 29600, electric: false }, + ], + // Column Definitions: Defines the columns to be displayed. + columnDefs: [ + { field: "make" }, + { field: "model" }, + { field: "price" }, + { field: "electric" } + ] + }); +} From 592e968f9f00cb47c6a505c312c7016de394d588 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 11:03:43 +0300 Subject: [PATCH 006/107] feat(book/table): display note titles --- .../src/widgets/view_widgets/table_view.ts | 9 ++++-- .../widgets/view_widgets/table_view/data.ts | 29 +++++++++++++++++++ .../view_widgets/table_view/renderer.ts | 19 +++++------- 3 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/data.ts diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 2f47f67e7..b87b72702 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -1,4 +1,5 @@ -import renderTable from "./table_view/renderer"; +import froca from "../../services/froca.js"; +import renderTable from "./table_view/renderer.js"; import ViewMode, { ViewModeArgs } from "./view_mode"; const TPL = /*html*/` @@ -27,12 +28,14 @@ export default class TableView extends ViewMode { private $root: JQuery; private $container: JQuery; + private noteIds: string[]; constructor(args: ViewModeArgs) { super(args); this.$root = $(TPL); this.$container = this.$root.find(".table-view-container"); + this.noteIds = args.noteIds; args.$parent.append(this.$root); } @@ -41,8 +44,10 @@ export default class TableView extends ViewMode { } async renderList() { + const notes = await froca.getNotes(this.noteIds); + this.$container.empty(); - renderTable(this.$container[0]); + renderTable(this.$container[0], notes); return this.$root; } diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts new file mode 100644 index 000000000..a9b164cc0 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -0,0 +1,29 @@ +import { GridOptions } from "ag-grid-community"; +import FNote from "../../../entities/fnote"; + +interface Data { + title: string; +} + +export function buildColumnDefinitions(): GridOptions["columnDefs"] { + return [ + { + field: "title" + } + ]; +} + +export function buildRowDefinitions(notes: FNote[]): GridOptions["rowData"] { + const definitions: GridOptions["rowData"] = []; + for (const note of notes) { + definitions.push(buildRowDefinition(note)); + } + + return definitions; +} + +export function buildRowDefinition(note: FNote): Data { + return { + title: note.title + } +} diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts index 31559bc10..385acd1bf 100644 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -1,21 +1,16 @@ import { createGrid, AllCommunityModule, ModuleRegistry } from "ag-grid-community"; +import { buildColumnDefinitions, buildRowDefinitions } from "./data.js"; +import FNote from "../../../entities/fnote.js"; ModuleRegistry.registerModules([ AllCommunityModule ]); -export default function renderTable(el: HTMLElement) { +export default function renderTable(el: HTMLElement, notes: FNote[]) { + const rowData = buildRowDefinitions(notes); + createGrid(el, { // Row Data: The data to be displayed. - rowData: [ - { make: "Tesla", model: "Model Y", price: 64950, electric: true }, - { make: "Ford", model: "F-Series", price: 33850, electric: false }, - { make: "Toyota", model: "Corolla", price: 29600, electric: false }, - ], + rowData: rowData, // Column Definitions: Defines the columns to be displayed. - columnDefs: [ - { field: "make" }, - { field: "model" }, - { field: "price" }, - { field: "electric" } - ] + columnDefs: buildColumnDefinitions() }); } From 05aa0878515211bbd4937350652a48224108c949 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 11:23:34 +0300 Subject: [PATCH 007/107] feat(book/table): support basic text columns --- .../src/widgets/view_widgets/table_view.ts | 9 +-- .../widgets/view_widgets/table_view/data.ts | 56 +++++++++++++++---- .../view_widgets/table_view/renderer.ts | 11 +--- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index b87b72702..0505f09c7 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -28,14 +28,14 @@ export default class TableView extends ViewMode { private $root: JQuery; private $container: JQuery; - private noteIds: string[]; + private args: ViewModeArgs; constructor(args: ViewModeArgs) { super(args); this.$root = $(TPL); this.$container = this.$root.find(".table-view-container"); - this.noteIds = args.noteIds; + this.args = args; args.$parent.append(this.$root); } @@ -44,10 +44,11 @@ export default class TableView extends ViewMode { } async renderList() { - const notes = await froca.getNotes(this.noteIds); + const { noteIds, parentNote } = this.args; + const notes = await froca.getNotes(noteIds); this.$container.empty(); - renderTable(this.$container[0], notes); + renderTable(this.$container[0], parentNote, notes); return this.$root; } diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index a9b164cc0..be6a968e6 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,29 +1,63 @@ import { GridOptions } from "ag-grid-community"; import FNote from "../../../entities/fnote"; -interface Data { +type Data = { title: string; +} & Record; + +export function buildData(parentNote: FNote, notes: FNote[]) { + const { columnDefs, expectedLabels } = buildColumnDefinitions(parentNote); + const rowData = buildRowDefinitions(notes, expectedLabels); + + return { + rowData, + columnDefs + } } -export function buildColumnDefinitions(): GridOptions["columnDefs"] { - return [ +export function buildColumnDefinitions(parentNote: FNote) { + const columnDefs: GridOptions["columnDefs"] = [ { field: "title" } ]; + + const expectedLabels: string[] = []; + + for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { + console.log(promotedAttribute); + if (promotedAttribute.type !== "label") { + console.warn("Relations are not supported for now"); + continue; + } + + const def = promotedAttribute.getDefinition(); + const attributeName = promotedAttribute.name.split(":", 2)[1]; + const title = def.promotedAlias ?? attributeName; + + columnDefs.push({ + field: attributeName, + headerName: title + }); + expectedLabels.push(attributeName); + } + + return { columnDefs, expectedLabels }; } -export function buildRowDefinitions(notes: FNote[]): GridOptions["rowData"] { +export function buildRowDefinitions(notes: FNote[], expectedLabels: string[]): GridOptions["rowData"] { const definitions: GridOptions["rowData"] = []; for (const note of notes) { - definitions.push(buildRowDefinition(note)); + const data = { + title: note.title + }; + + for (const expectedLabel of expectedLabels) { + data[expectedLabel] = note.getLabelValue(expectedLabel); + } + + definitions.push(data); } return definitions; } - -export function buildRowDefinition(note: FNote): Data { - return { - title: note.title - } -} diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts index 385acd1bf..b8e9b92ca 100644 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -1,16 +1,11 @@ import { createGrid, AllCommunityModule, ModuleRegistry } from "ag-grid-community"; -import { buildColumnDefinitions, buildRowDefinitions } from "./data.js"; +import { buildData } from "./data.js"; import FNote from "../../../entities/fnote.js"; ModuleRegistry.registerModules([ AllCommunityModule ]); -export default function renderTable(el: HTMLElement, notes: FNote[]) { - const rowData = buildRowDefinitions(notes); - +export default function renderTable(el: HTMLElement, parentNote: FNote, notes: FNote[]) { createGrid(el, { - // Row Data: The data to be displayed. - rowData: rowData, - // Column Definitions: Defines the columns to be displayed. - columnDefs: buildColumnDefinitions() + ...buildData(parentNote, notes) }); } From 7c175da9f1ea1529d01071ef7567d788e3c2d2eb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 11:45:46 +0300 Subject: [PATCH 008/107] chore(book/table): ignore multi attributes --- apps/client/src/widgets/view_widgets/table_view/data.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index be6a968e6..e2ec94b92 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -32,6 +32,11 @@ export function buildColumnDefinitions(parentNote: FNote) { } const def = promotedAttribute.getDefinition(); + if (def.multiplicity !== "single") { + console.warn("Multiple values are not supported for now"); + continue; + } + const attributeName = promotedAttribute.name.split(":", 2)[1]; const title = def.promotedAlias ?? attributeName; From d9443527eea2c16a2436b34191472b1280047530 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 11:56:30 +0300 Subject: [PATCH 009/107] feat(book/table): support date type --- .../promoted_attribute_definition_parser.ts | 2 +- .../widgets/view_widgets/table_view/data.ts | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/client/src/services/promoted_attribute_definition_parser.ts b/apps/client/src/services/promoted_attribute_definition_parser.ts index f46040e2a..6cdb8edc6 100644 --- a/apps/client/src/services/promoted_attribute_definition_parser.ts +++ b/apps/client/src/services/promoted_attribute_definition_parser.ts @@ -1,4 +1,4 @@ -type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; +export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; type Multiplicity = "single" | "multi"; export interface DefinitionObject { diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index e2ec94b92..69a2aed03 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,10 +1,13 @@ import { GridOptions } from "ag-grid-community"; import FNote from "../../../entities/fnote"; +import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; type Data = { title: string; } & Record; +type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; + export function buildData(parentNote: FNote, notes: FNote[]) { const { columnDefs, expectedLabels } = buildColumnDefinitions(parentNote); const rowData = buildRowDefinitions(notes, expectedLabels); @@ -42,7 +45,8 @@ export function buildColumnDefinitions(parentNote: FNote) { columnDefs.push({ field: attributeName, - headerName: title + headerName: title, + cellDataType: mapDataType(def.labelType) }); expectedLabels.push(attributeName); } @@ -50,6 +54,20 @@ export function buildColumnDefinitions(parentNote: FNote) { return { columnDefs, expectedLabels }; } +function mapDataType(labelType: LabelType | undefined): GridLabelType { + if (!labelType) { + return "text"; + } + + switch (labelType) { + case "date": + return "dateString"; + case "text": + default: + return "text" + } +} + export function buildRowDefinitions(notes: FNote[], expectedLabels: string[]): GridOptions["rowData"] { const definitions: GridOptions["rowData"] = []; for (const note of notes) { From b6398fdb5dc216823a2f6da5468e24909b6b568c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 12:03:17 +0300 Subject: [PATCH 010/107] refactor(book/table): extract gathering definitions --- .../widgets/view_widgets/table_view/data.ts | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 69a2aed03..c08455e2b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -6,11 +6,18 @@ type Data = { title: string; } & Record; +interface PromotedAttributeInformation { + name: string; + title?: string; + type?: LabelType; +} + type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; export function buildData(parentNote: FNote, notes: FNote[]) { - const { columnDefs, expectedLabels } = buildColumnDefinitions(parentNote); - const rowData = buildRowDefinitions(notes, expectedLabels); + const info = getPromotedAttributeInformation(parentNote); + const columnDefs = buildColumnDefinitions(parentNote, info); + const rowData = buildRowDefinitions(notes, info); return { rowData, @@ -18,15 +25,26 @@ export function buildData(parentNote: FNote, notes: FNote[]) { } } -export function buildColumnDefinitions(parentNote: FNote) { +export function buildColumnDefinitions(parentNote: FNote, info: PromotedAttributeInformation[]) { const columnDefs: GridOptions["columnDefs"] = [ { field: "title" } ]; - const expectedLabels: string[] = []; + for (const { name, title, type } of info) { + columnDefs.push({ + field: name, + headerName: title, + cellDataType: mapDataType(type) + }); + } + return columnDefs; +} + +function getPromotedAttributeInformation(parentNote: FNote) { + const info: PromotedAttributeInformation[] = []; for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { console.log(promotedAttribute); if (promotedAttribute.type !== "label") { @@ -40,18 +58,13 @@ export function buildColumnDefinitions(parentNote: FNote) { continue; } - const attributeName = promotedAttribute.name.split(":", 2)[1]; - const title = def.promotedAlias ?? attributeName; - - columnDefs.push({ - field: attributeName, - headerName: title, - cellDataType: mapDataType(def.labelType) - }); - expectedLabels.push(attributeName); + info.push({ + name: promotedAttribute.name.split(":", 2)[1], + title: def.promotedAlias, + type: def.labelType + }) } - - return { columnDefs, expectedLabels }; + return info; } function mapDataType(labelType: LabelType | undefined): GridLabelType { @@ -60,6 +73,10 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType { } switch (labelType) { + case "number": + return "number"; + case "boolean": + return "boolean"; case "date": return "dateString"; case "text": @@ -68,15 +85,15 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType { } } -export function buildRowDefinitions(notes: FNote[], expectedLabels: string[]): GridOptions["rowData"] { +export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInformation[]): GridOptions["rowData"] { const definitions: GridOptions["rowData"] = []; for (const note of notes) { const data = { title: note.title }; - for (const expectedLabel of expectedLabels) { - data[expectedLabel] = note.getLabelValue(expectedLabel); + for (const info of infos) { + data[info.name] = note.getLabelValue(info.name); } definitions.push(data); From fb32d264795ddddcb1de66cbfbb3a11975ca9c47 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 12:05:10 +0300 Subject: [PATCH 011/107] feat(book/table): support boolean type --- apps/client/src/widgets/view_widgets/table_view/data.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index c08455e2b..03f6d7fd4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -92,8 +92,12 @@ export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInfo title: note.title }; - for (const info of infos) { - data[info.name] = note.getLabelValue(info.name); + for (const { name, type } of infos) { + if (type === "boolean") { + data[name] = note.hasLabel(name); + } else { + data[name] = note.getLabelValue(name); + } } definitions.push(data); From 66761a69d32cc4f787c5d13b075c9aeebb745b51 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 12:10:08 +0300 Subject: [PATCH 012/107] refactor(book/table): clean up --- apps/client/src/widgets/view_widgets/table_view/data.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 03f6d7fd4..046cef52b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -16,7 +16,7 @@ type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'ob export function buildData(parentNote: FNote, notes: FNote[]) { const info = getPromotedAttributeInformation(parentNote); - const columnDefs = buildColumnDefinitions(parentNote, info); + const columnDefs = buildColumnDefinitions(info); const rowData = buildRowDefinitions(notes, info); return { @@ -25,7 +25,7 @@ export function buildData(parentNote: FNote, notes: FNote[]) { } } -export function buildColumnDefinitions(parentNote: FNote, info: PromotedAttributeInformation[]) { +export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { const columnDefs: GridOptions["columnDefs"] = [ { field: "title" @@ -46,7 +46,6 @@ export function buildColumnDefinitions(parentNote: FNote, info: PromotedAttribut function getPromotedAttributeInformation(parentNote: FNote) { const info: PromotedAttributeInformation[] = []; for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { - console.log(promotedAttribute); if (promotedAttribute.type !== "label") { console.warn("Relations are not supported for now"); continue; From 7e20e41521f091218f84081c4e8e06bcf13d07b7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 13:06:38 +0300 Subject: [PATCH 013/107] feat(book/table): allow editing cell values --- apps/client/src/services/attributes.ts | 2 +- .../widgets/view_widgets/table_view/data.ts | 50 ++++++------------- .../widgets/view_widgets/table_view/parser.ts | 31 ++++++++++++ .../view_widgets/table_view/renderer.ts | 30 +++++++++-- 4 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/parser.ts diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 8de409e9b..315dc2c15 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -11,7 +11,7 @@ async function addLabel(noteId: string, name: string, value: string = "") { }); } -async function setLabel(noteId: string, name: string, value: string = "") { +export async function setLabel(noteId: string, name: string, value: string = "") { await server.put(`notes/${noteId}/set-attribute`, { type: "label", name: name, diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 046cef52b..1a7d6fd08 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,21 +1,17 @@ import { GridOptions } from "ag-grid-community"; -import FNote from "../../../entities/fnote"; +import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; +import { default as getPromotedAttributeInformation, type PromotedAttributeInformation } from "./parser.js"; -type Data = { +export type TableData = { + noteId: string; title: string; } & Record; -interface PromotedAttributeInformation { - name: string; - title?: string; - type?: LabelType; -} type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; -export function buildData(parentNote: FNote, notes: FNote[]) { - const info = getPromotedAttributeInformation(parentNote); +export function buildData(info: PromotedAttributeInformation[], notes: FNote[]) { const columnDefs = buildColumnDefinitions(info); const rowData = buildRowDefinitions(notes, info); @@ -26,7 +22,10 @@ export function buildData(parentNote: FNote, notes: FNote[]) { } export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { - const columnDefs: GridOptions["columnDefs"] = [ + const columnDefs: GridOptions["columnDefs"] = [ + { + field: "noteId" + }, { field: "title" } @@ -36,36 +35,14 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { columnDefs.push({ field: name, headerName: title, - cellDataType: mapDataType(type) + cellDataType: mapDataType(type), + editable: true }); } return columnDefs; } -function getPromotedAttributeInformation(parentNote: FNote) { - const info: PromotedAttributeInformation[] = []; - for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { - if (promotedAttribute.type !== "label") { - console.warn("Relations are not supported for now"); - continue; - } - - const def = promotedAttribute.getDefinition(); - if (def.multiplicity !== "single") { - console.warn("Multiple values are not supported for now"); - continue; - } - - info.push({ - name: promotedAttribute.name.split(":", 2)[1], - title: def.promotedAlias, - type: def.labelType - }) - } - return info; -} - function mapDataType(labelType: LabelType | undefined): GridLabelType { if (!labelType) { return "text"; @@ -84,10 +61,11 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType { } } -export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInformation[]): GridOptions["rowData"] { - const definitions: GridOptions["rowData"] = []; +export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInformation[]) { + const definitions: GridOptions["rowData"] = []; for (const note of notes) { const data = { + noteId: note.noteId, title: note.title }; diff --git a/apps/client/src/widgets/view_widgets/table_view/parser.ts b/apps/client/src/widgets/view_widgets/table_view/parser.ts new file mode 100644 index 000000000..7719948b6 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/parser.ts @@ -0,0 +1,31 @@ +import FNote from "../../../entities/fnote"; +import { LabelType } from "../../../services/promoted_attribute_definition_parser"; + +export interface PromotedAttributeInformation { + name: string; + title?: string; + type?: LabelType; +} + +export default function getPromotedAttributeInformation(parentNote: FNote) { + const info: PromotedAttributeInformation[] = []; + for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { + if (promotedAttribute.type !== "label") { + console.warn("Relations are not supported for now"); + continue; + } + + const def = promotedAttribute.getDefinition(); + if (def.multiplicity !== "single") { + console.warn("Multiple values are not supported for now"); + continue; + } + + info.push({ + name: promotedAttribute.name.split(":", 2)[1], + title: def.promotedAlias, + type: def.labelType + }) + } + return info; +} diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts index b8e9b92ca..8a9fc12cc 100644 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -1,11 +1,35 @@ -import { createGrid, AllCommunityModule, ModuleRegistry } from "ag-grid-community"; -import { buildData } from "./data.js"; +import { createGrid, AllCommunityModule, ModuleRegistry, columnDropStyleBordered, GridOptions } from "ag-grid-community"; +import { buildData, type TableData } from "./data.js"; import FNote from "../../../entities/fnote.js"; +import getPromotedAttributeInformation, { PromotedAttributeInformation } from "./parser.js"; +import { setLabel } from "../../../services/attributes.js"; ModuleRegistry.registerModules([ AllCommunityModule ]); export default function renderTable(el: HTMLElement, parentNote: FNote, notes: FNote[]) { + const info = getPromotedAttributeInformation(parentNote); + createGrid(el, { - ...buildData(parentNote, notes) + ...buildData(info, notes), + ...setupEditing(info) }); } + +function setupEditing(info: PromotedAttributeInformation[]): GridOptions { + return { + onCellValueChanged(event) { + if (event.type !== "cellValueChanged") { + return; + } + + const noteId = event.data.noteId; + const name = event.colDef.field; + if (!name) { + return; + } + + const { newValue } = event; + setLabel(noteId, name, newValue); + } + } +} From c7b16cd04390765f41975f8806b25bbd346e47c5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 13:52:53 +0300 Subject: [PATCH 014/107] feat(book/table): allow show/hide columns --- apps/client/src/menus/context_menu.ts | 14 ++++-- .../table_view/header-customization.ts | 49 +++++++++++++++++++ .../view_widgets/table_view/renderer.ts | 6 ++- 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/header-customization.ts diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index 72519233a..a23421651 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js"; import note_tooltip from "../services/note_tooltip.js"; import utils from "../services/utils.js"; -interface ContextMenuOptions { +export interface ContextMenuOptions { x: number; y: number; orientation?: "left"; @@ -28,6 +28,7 @@ export interface MenuCommandItem { items?: MenuItem[] | null; shortcut?: string; spellingSuggestion?: string; + checked?: boolean; } export type MenuItem = MenuCommandItem | MenuSeparatorItem; @@ -146,10 +147,13 @@ class ContextMenu { } else { const $icon = $(""); - if ("uiIcon" in item && item.uiIcon) { - $icon.addClass(item.uiIcon); - } else { - $icon.append(" "); + if ("uiIcon" in item || "checked" in item) { + const icon = (item.checked ? "bx bx-check" : item.uiIcon); + if (icon) { + $icon.addClass(icon); + } else { + $icon.append(" "); + } } const $link = $("") diff --git a/apps/client/src/widgets/view_widgets/table_view/header-customization.ts b/apps/client/src/widgets/view_widgets/table_view/header-customization.ts new file mode 100644 index 000000000..6907a3664 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/header-customization.ts @@ -0,0 +1,49 @@ +import { GridApi } from "ag-grid-community"; +import contextMenu, { MenuItem } from "../../../menus/context_menu.js"; +import { TableData } from "./data.js"; + +export default function applyHeaderCustomization(baseEl: HTMLElement, api: GridApi) { + const header = baseEl.querySelector(".ag-header"); + if (!header) { + return; + } + + header.addEventListener("contextmenu", (e) => { + e.preventDefault(); + + + contextMenu.show({ + items: [ + { + title: "Columns", + items: buildColumnChooser(api) + } + ], + x: e.pageX, + y: e.pageY, + selectMenuItemHandler: () => {} + }); + }); +} + +export function buildColumnChooser(api: GridApi) { + const items: MenuItem[] = []; + + for (const column of api.getColumns() ?? []) { + const colDef = column.getColDef(); + if (!colDef) { + continue; + } + + const visible = column.isVisible(); + items.push({ + title: colDef.headerName ?? api.getDisplayNameForColumn(column, "header") ?? "", + checked: visible, + handler() { + api.setColumnsVisible([ column ], !visible); + } + }); + } + + return items; +} diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts index 8a9fc12cc..dbf27f13c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -3,6 +3,7 @@ import { buildData, type TableData } from "./data.js"; import FNote from "../../../entities/fnote.js"; import getPromotedAttributeInformation, { PromotedAttributeInformation } from "./parser.js"; import { setLabel } from "../../../services/attributes.js"; +import applyHeaderCustomization from "./header-customization.js"; ModuleRegistry.registerModules([ AllCommunityModule ]); @@ -11,7 +12,10 @@ export default function renderTable(el: HTMLElement, parentNote: FNote, notes: F createGrid(el, { ...buildData(info, notes), - ...setupEditing(info) + ...setupEditing(info), + onGridReady(event) { + applyHeaderCustomization(el, event.api); + }, }); } From ccb9b7e5fb5bbddcce42a3144687177923fe8b7d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 16:18:34 +0300 Subject: [PATCH 015/107] feat(book/table): store hidden columns --- .../src/widgets/view_widgets/calendar_view.ts | 6 +-- .../widgets/view_widgets/list_or_grid_view.ts | 11 ++--- .../src/widgets/view_widgets/table_view.ts | 7 +-- .../view_widgets/table_view/renderer.ts | 23 +++++++--- .../view_widgets/table_view/storage.ts | 6 +++ .../src/widgets/view_widgets/view_mode.ts | 22 +++++++++- .../widgets/view_widgets/view_mode_storage.ts | 43 +++++++++++++++++++ 7 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/storage.ts create mode 100644 apps/client/src/widgets/view_widgets/view_mode_storage.ts diff --git a/apps/client/src/widgets/view_widgets/calendar_view.ts b/apps/client/src/widgets/view_widgets/calendar_view.ts index 51758fca3..4d6a32913 100644 --- a/apps/client/src/widgets/view_widgets/calendar_view.ts +++ b/apps/client/src/widgets/view_widgets/calendar_view.ts @@ -109,24 +109,22 @@ const CALENDAR_VIEWS = [ "listMonth" ] -export default class CalendarView extends ViewMode { +export default class CalendarView extends ViewMode<{}> { private $root: JQuery; private $calendarContainer: JQuery; private noteIds: string[]; - private parentNote: FNote; private calendar?: Calendar; private isCalendarRoot: boolean; private lastView?: string; private debouncedSaveView?: DebouncedFunction<() => void>; constructor(args: ViewModeArgs) { - super(args); + super(args, "calendar"); this.$root = $(TPL); this.$calendarContainer = this.$root.find(".calendar-container"); this.noteIds = args.noteIds; - this.parentNote = args.parentNote; 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 54b83b971..c8236b053 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 @@ -6,6 +6,7 @@ import treeService from "../../services/tree.js"; import utils from "../../services/utils.js"; import type FNote from "../../entities/fnote.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; +import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; const TPL = /*html*/`
@@ -157,26 +158,22 @@ const TPL = /*html*/`
`; -class ListOrGridView extends ViewMode { +class ListOrGridView extends ViewMode<{}> { private $noteList: JQuery; - private parentNote: FNote; private noteIds: string[]; private page?: number; private pageSize?: number; - private viewType?: string | null; private showNotePath?: boolean; private highlightRegex?: RegExp | null; /* * We're using noteIds so that it's not necessary to load all notes at once when paging */ - constructor(viewType: string, args: ViewModeArgs) { - super(args); + constructor(viewType: ViewTypeOptions, args: ViewModeArgs) { + super(args, viewType); this.$noteList = $(TPL); - this.viewType = viewType; - this.parentNote = args.parentNote; const includedNoteIds = this.getIncludedNoteIds(); this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 0505f09c7..05c4c7188 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -1,5 +1,6 @@ import froca from "../../services/froca.js"; import renderTable from "./table_view/renderer.js"; +import type { StateInfo } from "./table_view/storage.js"; import ViewMode, { ViewModeArgs } from "./view_mode"; const TPL = /*html*/` @@ -24,14 +25,14 @@ const TPL = /*html*/` `; -export default class TableView extends ViewMode { +export default class TableView extends ViewMode { private $root: JQuery; private $container: JQuery; private args: ViewModeArgs; constructor(args: ViewModeArgs) { - super(args); + super(args, "table"); this.$root = $(TPL); this.$container = this.$root.find(".table-view-container"); @@ -48,7 +49,7 @@ export default class TableView extends ViewMode { const notes = await froca.getNotes(noteIds); this.$container.empty(); - renderTable(this.$container[0], parentNote, notes); + renderTable(this.$container[0], parentNote, notes, this.viewStorage); return this.$root; } diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts index dbf27f13c..18ed838a4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts @@ -1,25 +1,35 @@ -import { createGrid, AllCommunityModule, ModuleRegistry, columnDropStyleBordered, GridOptions } from "ag-grid-community"; +import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; import { buildData, type TableData } from "./data.js"; import FNote from "../../../entities/fnote.js"; -import getPromotedAttributeInformation, { PromotedAttributeInformation } from "./parser.js"; +import getPromotedAttributeInformation from "./parser.js"; import { setLabel } from "../../../services/attributes.js"; import applyHeaderCustomization from "./header-customization.js"; +import ViewModeStorage from "../view_mode_storage.js"; +import { type StateInfo } from "./storage.js"; ModuleRegistry.registerModules([ AllCommunityModule ]); -export default function renderTable(el: HTMLElement, parentNote: FNote, notes: FNote[]) { +export default async function renderTable(el: HTMLElement, parentNote: FNote, notes: FNote[], storage: ViewModeStorage) { const info = getPromotedAttributeInformation(parentNote); + const viewStorage = await storage.restore(); + const initialState = viewStorage?.gridState; createGrid(el, { ...buildData(info, notes), - ...setupEditing(info), - onGridReady(event) { + ...setupEditing(), + initialState, + async onGridReady(event) { applyHeaderCustomization(el, event.api); }, + onStateUpdated(event) { + storage.store({ + gridState: event.api.getState() + }); + } }); } -function setupEditing(info: PromotedAttributeInformation[]): GridOptions { +function setupEditing(): GridOptions { return { onCellValueChanged(event) { if (event.type !== "cellValueChanged") { @@ -37,3 +47,4 @@ function setupEditing(info: PromotedAttributeInformation[]): GridOptions; @@ -8,11 +10,18 @@ export interface ViewModeArgs { showNotePath?: boolean; } -export default abstract class ViewMode { +export default abstract class ViewMode { - constructor(args: ViewModeArgs) { + private _viewStorage: ViewModeStorage | null; + protected parentNote: FNote; + protected viewType: ViewTypeOptions; + + constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { + this.parentNote = args.parentNote; + this._viewStorage = null; // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work args.$parent.empty(); + this.viewType = viewType; } abstract renderList(): Promise | undefined>; @@ -32,4 +41,13 @@ export default abstract class ViewMode { return false; } + get viewStorage() { + if (this._viewStorage) { + return this._viewStorage; + } + + this._viewStorage = new ViewModeStorage(this.parentNote, this.viewType); + return this._viewStorage; + } + } diff --git a/apps/client/src/widgets/view_widgets/view_mode_storage.ts b/apps/client/src/widgets/view_widgets/view_mode_storage.ts new file mode 100644 index 000000000..750e9d9b0 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/view_mode_storage.ts @@ -0,0 +1,43 @@ +import type FNote from "../../entities/fnote"; +import type { ViewTypeOptions } from "../../services/note_list_renderer"; +import server from "../../services/server"; + +const ATTACHMENT_ROLE = "viewConfig"; + +export default class ViewModeStorage { + + private note: FNote; + private attachmentName: string; + + constructor(note: FNote, viewType: ViewTypeOptions) { + this.note = note; + this.attachmentName = viewType + ".json"; + } + + async store(data: T) { + const payload = { + role: ATTACHMENT_ROLE, + title: this.attachmentName, + mime: "application/json", + content: JSON.stringify(data), + position: 0 + }; + await server.post(`notes/${this.note.noteId}/attachments?matchBy=title`, payload); + } + + async restore() { + const existingAttachments = await this.note.getAttachmentsByRole(ATTACHMENT_ROLE); + if (existingAttachments.length === 0) { + return undefined; + } + + const attachment = existingAttachments + .find(a => a.title === this.attachmentName); + if (!attachment) { + return undefined; + } + + const attachmentData = await server.get<{ content: string } | null>(`attachments/${attachment.attachmentId}/blob`); + return JSON.parse(attachmentData?.content ?? "{}"); + } +} From 9c137a1c48b3a16f1b211ee4aa0b987212a0b2f3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 17:43:58 +0300 Subject: [PATCH 016/107] feat(book/table): display attachment JSON --- apps/client/src/services/content_renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 08ed561ff..12c8a21f4 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -301,7 +301,7 @@ function getRenderingType(entity: FNote | FAttachment) { if (type === "file" && mime === "application/pdf") { type = "pdf"; - } else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) { + } else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) { type = "code"; } else if (type === "file" && mime && mime.startsWith("audio/")) { type = "audio"; From 9e57c141300e06d304b3cb00e63d015593846d1c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Jun 2025 17:45:11 +0300 Subject: [PATCH 017/107] feat(attachments): add pretty formatting to JSON --- apps/client/src/services/content_renderer.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 12c8a21f4..40480e073 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -118,8 +118,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery) { const blob = await note.getBlob(); + let content = blob?.content || ""; + if (note.mime === "application/json") { + try { + content = JSON.stringify(JSON.parse(content), null, 4); + } catch (e) { + // Ignore JSON parsing errors. + } + } + const $codeBlock = $(""); - $codeBlock.text(blob?.content || ""); + $codeBlock.text(content); $renderedContent.append($("
").append($codeBlock));
     await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
 }

From 168e224d3e1c726ca1215b0af12dd5433bef15f5 Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 17:54:00 +0300
Subject: [PATCH 018/107] refactor(book/table): make clear what kind of
 attribute is being changed

---
 .../widgets/view_widgets/table_view/data.ts   | 22 +++++++++----------
 .../view_widgets/table_view/renderer.ts       |  7 ++++--
 2 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts
index 1a7d6fd08..7a8a461d8 100644
--- a/apps/client/src/widgets/view_widgets/table_view/data.ts
+++ b/apps/client/src/widgets/view_widgets/table_view/data.ts
@@ -6,7 +6,8 @@ import { default as getPromotedAttributeInformation, type PromotedAttributeInfor
 export type TableData = {
     noteId: string;
     title: string;
-} & Record;
+    labels: Record;
+};
 
 
 type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object';
@@ -33,7 +34,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) {
 
     for (const { name, title, type } of info) {
         columnDefs.push({
-            field: name,
+            field: `labels.${name}`,
             headerName: title,
             cellDataType: mapDataType(type),
             editable: true
@@ -64,20 +65,19 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType {
 export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInformation[]) {
     const definitions: GridOptions["rowData"] = [];
     for (const note of notes) {
-        const data = {
-            noteId: note.noteId,
-            title: note.title
-        };
-
+        const labels: typeof definitions[0]["labels"] = {};
         for (const { name, type } of infos) {
             if (type === "boolean") {
-                data[name] = note.hasLabel(name);
+                labels[name] = note.hasLabel(name);
             } else {
-                data[name] = note.getLabelValue(name);
+                labels[name] = note.getLabelValue(name);
             }
         }
-
-        definitions.push(data);
+        definitions.push({
+            noteId: note.noteId,
+            title: note.title,
+            labels
+        });
     }
 
     return definitions;
diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts
index 18ed838a4..c4e7384b4 100644
--- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts
+++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts
@@ -42,8 +42,11 @@ function setupEditing(): GridOptions {
                 return;
             }
 
-            const { newValue } = event;
-            setLabel(noteId, name, newValue);
+            if (name.startsWith("labels.")) {
+                const { newValue } = event;
+                const labelName = name.split(".", 2)[1];
+                setLabel(noteId, labelName, newValue);
+            }
         }
     }
 }

From c9b556160fb4bbbceaf56ce7dce7cfea94b523cc Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 17:56:47 +0300
Subject: [PATCH 019/107] feat(book/table): support changing note title

---
 apps/client/src/widgets/view_widgets/table_view/data.ts   | 6 ++++--
 .../src/widgets/view_widgets/table_view/renderer.ts       | 8 +++++++-
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts
index 7a8a461d8..126a8e537 100644
--- a/apps/client/src/widgets/view_widgets/table_view/data.ts
+++ b/apps/client/src/widgets/view_widgets/table_view/data.ts
@@ -25,10 +25,12 @@ export function buildData(info: PromotedAttributeInformation[], notes: FNote[])
 export function buildColumnDefinitions(info: PromotedAttributeInformation[]) {
     const columnDefs: GridOptions["columnDefs"] = [
         {
-            field: "noteId"
+            field: "noteId",
+            editable: false
         },
         {
-            field: "title"
+            field: "title",
+            editable: true
         }
     ];
 
diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts
index c4e7384b4..b6ec442c4 100644
--- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts
+++ b/apps/client/src/widgets/view_widgets/table_view/renderer.ts
@@ -6,6 +6,7 @@ import { setLabel } from "../../../services/attributes.js";
 import applyHeaderCustomization from "./header-customization.js";
 import ViewModeStorage from "../view_mode_storage.js";
 import { type StateInfo } from "./storage.js";
+import server from "../../../services/server.js";
 
 ModuleRegistry.registerModules([ AllCommunityModule ]);
 
@@ -42,8 +43,13 @@ function setupEditing(): GridOptions {
                 return;
             }
 
+            const { newValue } = event;
+            if (name === "title") {
+                // TODO: Deduplicate with note_title.
+                server.put(`notes/${noteId}/title`, { title: newValue });
+            }
+
             if (name.startsWith("labels.")) {
-                const { newValue } = event;
                 const labelName = name.split(".", 2)[1];
                 setLabel(noteId, labelName, newValue);
             }

From dd379bf18d52402c342a0ba0f99cf680cc380521 Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 18:30:44 +0300
Subject: [PATCH 020/107] refactor(book/table): fix some lack of generics

---
 apps/client/src/services/note_list_renderer.ts | 2 +-
 apps/client/src/widgets/note_list.ts           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts
index a55e93c8a..04dc5b132 100644
--- a/apps/client/src/services/note_list_renderer.ts
+++ b/apps/client/src/services/note_list_renderer.ts
@@ -10,7 +10,7 @@ export type ViewTypeOptions = "list" | "grid" | "calendar" | "table";
 export default class NoteListRenderer {
 
     private viewType: ViewTypeOptions;
-    public viewMode: ViewMode | null;
+    public viewMode: ViewMode | null;
 
     constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
         this.viewType = this.#getViewType(parentNote);
diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts
index 73ebb358d..ba1a028f2 100644
--- a/apps/client/src/widgets/note_list.ts
+++ b/apps/client/src/widgets/note_list.ts
@@ -36,7 +36,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
     private isIntersecting?: boolean;
     private noteIdRefreshed?: string;
     private shownNoteId?: string | null;
-    private viewMode?: ViewMode | null;
+    private viewMode?: ViewMode | null;
 
     isEnabled() {
         return super.isEnabled() && this.noteContext?.hasNoteList();

From dcb4ebe5d9b49a2eaa9af2a937963a5c1a2e2c72 Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 18:31:45 +0300
Subject: [PATCH 021/107] feat(book/table): display even if empty

---
 apps/client/src/components/note_context.ts | 40 +++++++++++++++++-----
 1 file changed, 32 insertions(+), 8 deletions(-)

diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts
index 3a8a54310..020817073 100644
--- a/apps/client/src/components/note_context.ts
+++ b/apps/client/src/components/note_context.ts
@@ -315,14 +315,38 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
     }
 
     hasNoteList() {
-        return (
-            this.note &&
-            ["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
-            (this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
-            ["book", "text", "code"].includes(this.note.type) &&
-            this.note.mime !== "text/x-sqlite;schema=trilium" &&
-            !this.note.isLabelTruthy("hideChildrenOverview")
-        );
+        const note = this.note;
+
+        if (!note) {
+            return false;
+        }
+
+        if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
+            return false;
+        }
+
+        // Some book types must always display a note list, even if no children.
+        if (["calendar", "table"].includes(note.getLabelValue("viewType") ?? "")) {
+            return true;
+        }
+
+        if (!note.hasChildren()) {
+            return false;
+        }
+
+        if (!["book", "text", "code"].includes(note.type)) {
+            return false;
+        }
+
+        if (note.mime === "text/x-sqlite;schema=trilium") {
+            return false;
+        }
+
+        if (note.isLabelTruthy("hideChildrenOverview")) {
+            return false;
+        }
+
+        return true;
     }
 
     async getTextEditor(callback?: GetTextEditorCallback) {

From 4a22e3d2d4dcda19e0132f91ec526347c9909465 Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 19:25:01 +0300
Subject: [PATCH 022/107] feat(book/table): hide promoted attributes

---
 .../ribbon_widgets/promoted_attributes.ts     |  2 +-
 .../table_view/header-add-column-button.ts    | 46 +++++++++++++++++++
 2 files changed, 47 insertions(+), 1 deletion(-)
 create mode 100644 apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts

diff --git a/apps/client/src/widgets/ribbon_widgets/promoted_attributes.ts b/apps/client/src/widgets/ribbon_widgets/promoted_attributes.ts
index d94cfdbd1..33341dd3f 100644
--- a/apps/client/src/widgets/ribbon_widgets/promoted_attributes.ts
+++ b/apps/client/src/widgets/ribbon_widgets/promoted_attributes.ts
@@ -117,7 +117,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
         // the order of attributes is important as well
         ownedAttributes.sort((a, b) => a.position - b.position);
 
-        if (promotedDefAttrs.length === 0) {
+        if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
             this.toggleInt(false);
             return;
         }
diff --git a/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts b/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts
new file mode 100644
index 000000000..cf02950c3
--- /dev/null
+++ b/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts
@@ -0,0 +1,46 @@
+import {
+    IHeaderParams,
+    IHeaderComp,
+} from 'ag-grid-community';
+
+export default class TableAddColumnButton implements IHeaderComp {
+    private eGui!: HTMLElement;
+    private params!: IHeaderParams;
+
+    public init(params: IHeaderParams): void {
+        this.params = params;
+
+        const container = document.createElement('div');
+        container.style.display = 'flex';
+        container.style.justifyContent = 'space-between';
+        container.style.alignItems = 'center';
+
+        const label = document.createElement('span');
+        label.innerText = params.displayName;
+
+        const button = document.createElement('button');
+        button.textContent = '+';
+        button.title = 'Add Row';
+        button.onclick = () => {
+            alert(`Add row for column: ${params.displayName}`);
+            // Optionally trigger insert logic here
+        };
+
+        container.appendChild(label);
+        container.appendChild(button);
+
+        this.eGui = container;
+    }
+
+    public getGui(): HTMLElement {
+        return this.eGui;
+    }
+
+    refresh(params: IHeaderParams): boolean {
+        return false;
+    }
+
+    public destroy(): void {
+        // Optional: clean up if needed
+    }
+}

From ecd3b7039f2f72a52cb37514ab3caea62c7b88ec Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Wed, 25 Jun 2025 19:31:25 +0300
Subject: [PATCH 023/107] feat(book/table): add template

---
 .../src/services/hidden_subtree_templates.ts    | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/apps/server/src/services/hidden_subtree_templates.ts b/apps/server/src/services/hidden_subtree_templates.ts
index 877da99c5..62c841898 100644
--- a/apps/server/src/services/hidden_subtree_templates.ts
+++ b/apps/server/src/services/hidden_subtree_templates.ts
@@ -26,6 +26,23 @@ export default function buildHiddenSubtreeTemplates() {
                         value: "promoted,alias=Description,single,text"
                     }
                 ]
+            },
+            {
+                id: "_template_table",
+                type: "book",
+                title: "Table",
+                icon: "bx bx-table",
+                attributes: [
+                    {
+                        name: "template",
+                        type: "label"
+                    },
+                    {
+                        name: "viewType",
+                        type: "label",
+                        value: "table"
+                    }
+                ]
             }
         ]
     };

From 70694542eb260dba6f2f9540ad5f089837750af6 Mon Sep 17 00:00:00 2001
From: Elian Doran 
Date: Fri, 27 Jun 2025 17:18:52 +0300
Subject: [PATCH 024/107] feat(views/table): allow in search

---
 apps/client/src/widgets/view_widgets/table_view.ts | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts
index 05c4c7188..f9b0163c6 100644
--- a/apps/client/src/widgets/view_widgets/table_view.ts
+++ b/apps/client/src/widgets/view_widgets/table_view.ts
@@ -17,6 +17,14 @@ const TPL = /*html*/`
     .table-view-container {
         height: 100%;
     }
+
+    .search-result-widget-content .table-view {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+    }
     
 
     
From 88b4fc73de0e40640ffce28d743c48df31ae534b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:22:47 +0300 Subject: [PATCH 025/107] chore(views/table): remove placeholder text --- apps/client/src/widgets/view_widgets/table_view.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index f9b0163c6..52bd42ea4 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -27,9 +27,7 @@ const TPL = /*html*/` } -
-

Table view goes here.

-
+
`; From 19eff5e6d6cc8bf1199c820741040fbb97c5a739 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:39:57 +0300 Subject: [PATCH 026/107] refactor(views/table): merge renderer into table view --- .../src/widgets/view_widgets/table_view.ts | 64 +++++++++++++++++-- .../view_widgets/table_view/renderer.ts | 59 ----------------- 2 files changed, 58 insertions(+), 65 deletions(-) delete mode 100644 apps/client/src/widgets/view_widgets/table_view/renderer.ts diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 52bd42ea4..f79b92380 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -1,7 +1,12 @@ import froca from "../../services/froca.js"; -import renderTable from "./table_view/renderer.js"; import type { StateInfo } from "./table_view/storage.js"; -import ViewMode, { ViewModeArgs } from "./view_mode"; +import ViewMode, { type ViewModeArgs } from "./view_mode.js"; +import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; +import { setLabel } from "../../services/attributes.js"; +import getPromotedAttributeInformation from "./table_view/parser.js"; +import { buildData, TableData } from "./table_view/data.js"; +import applyHeaderCustomization from "./table_view/header-customization.js"; +import server from "../../services/server.js"; const TPL = /*html*/`
@@ -44,6 +49,8 @@ export default class TableView extends ViewMode { this.$container = this.$root.find(".table-view-container"); this.args = args; args.$parent.append(this.$root); + + ModuleRegistry.registerModules([ AllCommunityModule ]); } get isFullHeight(): boolean { @@ -51,12 +58,57 @@ export default class TableView extends ViewMode { } async renderList() { - const { noteIds, parentNote } = this.args; - const notes = await froca.getNotes(noteIds); - this.$container.empty(); - renderTable(this.$container[0], parentNote, notes, this.viewStorage); + this.renderTable(this.$container[0]); return this.$root; } + private async renderTable(el: HTMLElement) { + const { noteIds, parentNote } = this.args; + const notes = await froca.getNotes(noteIds); + + const info = getPromotedAttributeInformation(parentNote); + const viewStorage = await this.viewStorage.restore(); + const initialState = viewStorage?.gridState; + + createGrid(el, { + ...buildData(info, notes), + ...setupEditing(), + initialState, + async onGridReady(event) { + applyHeaderCustomization(el, event.api); + }, + onStateUpdated: (event) => this.viewStorage.store({ + gridState: event.api.getState() + }) + }); + } + +} + +function setupEditing(): GridOptions { + return { + onCellValueChanged(event) { + if (event.type !== "cellValueChanged") { + return; + } + + const noteId = event.data.noteId; + const name = event.colDef.field; + if (!name) { + return; + } + + const { newValue } = event; + if (name === "title") { + // TODO: Deduplicate with note_title. + server.put(`notes/${noteId}/title`, { title: newValue }); + } + + if (name.startsWith("labels.")) { + const labelName = name.split(".", 2)[1]; + setLabel(noteId, labelName, newValue); + } + } + } } diff --git a/apps/client/src/widgets/view_widgets/table_view/renderer.ts b/apps/client/src/widgets/view_widgets/table_view/renderer.ts deleted file mode 100644 index b6ec442c4..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/renderer.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; -import { buildData, type TableData } from "./data.js"; -import FNote from "../../../entities/fnote.js"; -import getPromotedAttributeInformation from "./parser.js"; -import { setLabel } from "../../../services/attributes.js"; -import applyHeaderCustomization from "./header-customization.js"; -import ViewModeStorage from "../view_mode_storage.js"; -import { type StateInfo } from "./storage.js"; -import server from "../../../services/server.js"; - -ModuleRegistry.registerModules([ AllCommunityModule ]); - -export default async function renderTable(el: HTMLElement, parentNote: FNote, notes: FNote[], storage: ViewModeStorage) { - const info = getPromotedAttributeInformation(parentNote); - const viewStorage = await storage.restore(); - const initialState = viewStorage?.gridState; - - createGrid(el, { - ...buildData(info, notes), - ...setupEditing(), - initialState, - async onGridReady(event) { - applyHeaderCustomization(el, event.api); - }, - onStateUpdated(event) { - storage.store({ - gridState: event.api.getState() - }); - } - }); -} - -function setupEditing(): GridOptions { - return { - onCellValueChanged(event) { - if (event.type !== "cellValueChanged") { - return; - } - - const noteId = event.data.noteId; - const name = event.colDef.field; - if (!name) { - return; - } - - const { newValue } = event; - if (name === "title") { - // TODO: Deduplicate with note_title. - server.put(`notes/${noteId}/title`, { title: newValue }); - } - - if (name.startsWith("labels.")) { - const labelName = name.split(".", 2)[1]; - setLabel(noteId, labelName, newValue); - } - } - } -} - From e66aef17df89dba3bdc25f261524f332c018260e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:40:56 +0300 Subject: [PATCH 027/107] refactor(views/table): merge storage into table view --- apps/client/src/widgets/view_widgets/table_view.ts | 6 +++++- apps/client/src/widgets/view_widgets/table_view/storage.ts | 6 ------ 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 apps/client/src/widgets/view_widgets/table_view/storage.ts diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index f79b92380..420841781 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -1,5 +1,4 @@ import froca from "../../services/froca.js"; -import type { StateInfo } from "./table_view/storage.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; import { setLabel } from "../../services/attributes.js"; @@ -7,6 +6,7 @@ import getPromotedAttributeInformation from "./table_view/parser.js"; import { buildData, TableData } from "./table_view/data.js"; import applyHeaderCustomization from "./table_view/header-customization.js"; import server from "../../services/server.js"; +import type { GridState } from "ag-grid-community"; const TPL = /*html*/`
@@ -36,6 +36,10 @@ const TPL = /*html*/`
`; +export interface StateInfo { + gridState: GridState; +} + export default class TableView extends ViewMode { private $root: JQuery; diff --git a/apps/client/src/widgets/view_widgets/table_view/storage.ts b/apps/client/src/widgets/view_widgets/table_view/storage.ts deleted file mode 100644 index 415828fe5..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/storage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { GridState } from "ag-grid-community"; - -export interface StateInfo { - gridState: GridState; -} - From 0b74de275ca369a14e5b2604b72bed8926406a29 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:43:19 +0300 Subject: [PATCH 028/107] refactor(views/table): integrate parser into data --- .../src/widgets/view_widgets/table_view.ts | 3 +- .../widgets/view_widgets/table_view/data.ts | 29 ++++++++++++++++- .../widgets/view_widgets/table_view/parser.ts | 31 ------------------- 3 files changed, 29 insertions(+), 34 deletions(-) delete mode 100644 apps/client/src/widgets/view_widgets/table_view/parser.ts diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view.ts index 420841781..21304ba22 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view.ts @@ -2,8 +2,7 @@ import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; import { setLabel } from "../../services/attributes.js"; -import getPromotedAttributeInformation from "./table_view/parser.js"; -import { buildData, TableData } from "./table_view/data.js"; +import getPromotedAttributeInformation, { buildData, TableData } from "./table_view/data.js"; import applyHeaderCustomization from "./table_view/header-customization.js"; import server from "../../services/server.js"; import type { GridState } from "ag-grid-community"; diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 126a8e537..17257ba8b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,7 +1,6 @@ import { GridOptions } from "ag-grid-community"; import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; -import { default as getPromotedAttributeInformation, type PromotedAttributeInformation } from "./parser.js"; export type TableData = { noteId: string; @@ -9,6 +8,11 @@ export type TableData = { labels: Record; }; +export interface PromotedAttributeInformation { + name: string; + title?: string; + type?: LabelType; +} type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; @@ -84,3 +88,26 @@ export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInfo return definitions; } + +export default function getPromotedAttributeInformation(parentNote: FNote) { + const info: PromotedAttributeInformation[] = []; + for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { + if (promotedAttribute.type !== "label") { + console.warn("Relations are not supported for now"); + continue; + } + + const def = promotedAttribute.getDefinition(); + if (def.multiplicity !== "single") { + console.warn("Multiple values are not supported for now"); + continue; + } + + info.push({ + name: promotedAttribute.name.split(":", 2)[1], + title: def.promotedAlias, + type: def.labelType + }) + } + return info; +} diff --git a/apps/client/src/widgets/view_widgets/table_view/parser.ts b/apps/client/src/widgets/view_widgets/table_view/parser.ts deleted file mode 100644 index 7719948b6..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/parser.ts +++ /dev/null @@ -1,31 +0,0 @@ -import FNote from "../../../entities/fnote"; -import { LabelType } from "../../../services/promoted_attribute_definition_parser"; - -export interface PromotedAttributeInformation { - name: string; - title?: string; - type?: LabelType; -} - -export default function getPromotedAttributeInformation(parentNote: FNote) { - const info: PromotedAttributeInformation[] = []; - for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { - if (promotedAttribute.type !== "label") { - console.warn("Relations are not supported for now"); - continue; - } - - const def = promotedAttribute.getDefinition(); - if (def.multiplicity !== "single") { - console.warn("Multiple values are not supported for now"); - continue; - } - - info.push({ - name: promotedAttribute.name.split(":", 2)[1], - title: def.promotedAlias, - type: def.labelType - }) - } - return info; -} From c5020b88844d8b66518af10cf8960bd7edf15068 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:44:29 +0300 Subject: [PATCH 029/107] refactor(views/table): move table view into its own folder --- apps/client/src/services/note_list_renderer.ts | 2 +- .../{table_view.ts => table_view/index.ts} | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename apps/client/src/widgets/view_widgets/{table_view.ts => table_view/index.ts} (90%) diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index 04dc5b132..9518c3b79 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -1,7 +1,7 @@ import type FNote from "../entities/fnote.js"; import CalendarView from "../widgets/view_widgets/calendar_view.js"; import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; -import TableView from "../widgets/view_widgets/table_view.js"; +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"; diff --git a/apps/client/src/widgets/view_widgets/table_view.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts similarity index 90% rename from apps/client/src/widgets/view_widgets/table_view.ts rename to apps/client/src/widgets/view_widgets/table_view/index.ts index 21304ba22..d0a61b2df 100644 --- a/apps/client/src/widgets/view_widgets/table_view.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,10 +1,10 @@ -import froca from "../../services/froca.js"; -import ViewMode, { type ViewModeArgs } from "./view_mode.js"; +import froca from "../../../services/froca.js"; +import ViewMode, { type ViewModeArgs } from "../view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; -import { setLabel } from "../../services/attributes.js"; -import getPromotedAttributeInformation, { buildData, TableData } from "./table_view/data.js"; -import applyHeaderCustomization from "./table_view/header-customization.js"; -import server from "../../services/server.js"; +import { setLabel } from "../../../services/attributes.js"; +import getPromotedAttributeInformation, { buildData, TableData } from "./data.js"; +import applyHeaderCustomization from "./header-customization.js"; +import server from "../../../services/server.js"; import type { GridState } from "ag-grid-community"; const TPL = /*html*/` From 9dcd79bd94e2f8ae18a7ea1d0814c51fd5c742ee Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 17:58:25 +0300 Subject: [PATCH 030/107] feat(views/table): add debouncing --- .../widgets/view_widgets/table_view/index.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index d0a61b2df..720929e73 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -5,7 +5,8 @@ import { setLabel } from "../../../services/attributes.js"; import getPromotedAttributeInformation, { buildData, TableData } from "./data.js"; import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; -import type { GridState } from "ag-grid-community"; +import type { GridApi, GridState } from "ag-grid-community"; +import SpacedUpdate from "../../../services/spaced_update.js"; const TPL = /*html*/`
@@ -44,6 +45,8 @@ export default class TableView extends ViewMode { private $root: JQuery; private $container: JQuery; private args: ViewModeArgs; + private spacedUpdate: SpacedUpdate; + private api?: GridApi; constructor(args: ViewModeArgs) { super(args, "table"); @@ -51,6 +54,7 @@ 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); args.$parent.append(this.$root); ModuleRegistry.registerModules([ AllCommunityModule ]); @@ -74,16 +78,24 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - createGrid(el, { + this.api = createGrid(el, { ...buildData(info, notes), ...setupEditing(), initialState, async onGridReady(event) { applyHeaderCustomization(el, event.api); }, - onStateUpdated: (event) => this.viewStorage.store({ - gridState: event.api.getState() - }) + onStateUpdated: () => this.spacedUpdate.scheduleUpdate() + }); + } + + private onSave() { + if (!this.api) { + return; + } + + this.viewStorage.store({ + gridState: this.api.getState() }); } From 80d553650360fa14d2abbfd9c25874811c6325f4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 19:53:40 +0300 Subject: [PATCH 031/107] feat(views/table): basic drag support --- .../widgets/view_widgets/table_view/data.ts | 22 +++++++++---- .../widgets/view_widgets/table_view/index.ts | 32 ++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 17257ba8b..1ac64e364 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,11 +1,14 @@ import { GridOptions } from "ag-grid-community"; import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; +import froca from "../../../services/froca.js"; export type TableData = { noteId: string; title: string; labels: Record; + branchId: string; + position: number; }; export interface PromotedAttributeInformation { @@ -16,9 +19,9 @@ export interface PromotedAttributeInformation { type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; -export function buildData(info: PromotedAttributeInformation[], notes: FNote[]) { +export function buildData(parentNote: FNote, info: PromotedAttributeInformation[], notes: FNote[]) { const columnDefs = buildColumnDefinitions(info); - const rowData = buildRowDefinitions(notes, info); + const rowData = buildRowDefinitions(parentNote, notes, info); return { rowData, @@ -34,7 +37,11 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }, { field: "title", - editable: true + editable: true, + rowDrag: true, + }, + { + field: "position" } ]; @@ -68,9 +75,10 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType { } } -export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInformation[]) { +export function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { const definitions: GridOptions["rowData"] = []; - for (const note of notes) { + for (const branch of parentNote.getChildBranches()) { + const note = branch.getNoteFromCache(); const labels: typeof definitions[0]["labels"] = {}; for (const { name, type } of infos) { if (type === "boolean") { @@ -82,7 +90,9 @@ export function buildRowDefinitions(notes: FNote[], infos: PromotedAttributeInfo definitions.push({ noteId: note.noteId, title: note.title, - labels + labels, + position: branch.notePosition, + branchId: branch.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 720929e73..83bddbf99 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -7,6 +7,7 @@ import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; import type { GridApi, GridState } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; +import branches from "../../../services/branches.js"; const TPL = /*html*/`
@@ -79,8 +80,9 @@ export default class TableView extends ViewMode { const initialState = viewStorage?.gridState; this.api = createGrid(el, { - ...buildData(info, notes), + ...buildData(parentNote, info, notes), ...setupEditing(), + ...setupDragging(), initialState, async onGridReady(event) { applyHeaderCustomization(el, event.api); @@ -127,3 +129,31 @@ function setupEditing(): GridOptions { } } } + +function setupDragging() { + return { + onRowDragEnd(e) { + const fromIndex = e.node.rowIndex; + const toIndex = e.overNode?.rowIndex; + console.log(fromIndex, toIndex); + if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) { + return; + } + + const isBelow = (toIndex > fromIndex); + const fromBranchId = e.node.data?.branchId; + const toBranchId = e.overNode?.data?.branchId; + if (fromBranchId === undefined || toBranchId === undefined) { + return; + } + + if (isBelow) { + console.log("Move below", fromIndex, toIndex); + branches.moveAfterBranch([ fromBranchId ], toBranchId); + } else { + console.log("Move above", fromIndex, toIndex); + branches.moveBeforeBranch([ fromBranchId ], toBranchId); + } + } + }; +} From 6a0b24f03212e6308e2947d25714f8eb9b5e17c5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 20:08:41 +0300 Subject: [PATCH 032/107] chore(views/table): remove logs --- apps/client/src/widgets/view_widgets/table_view/index.ts | 3 --- 1 file changed, 3 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 83bddbf99..024509970 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -135,7 +135,6 @@ function setupDragging() { onRowDragEnd(e) { const fromIndex = e.node.rowIndex; const toIndex = e.overNode?.rowIndex; - console.log(fromIndex, toIndex); if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) { return; } @@ -148,10 +147,8 @@ function setupDragging() { } if (isBelow) { - console.log("Move below", fromIndex, toIndex); branches.moveAfterBranch([ fromBranchId ], toBranchId); } else { - console.log("Move above", fromIndex, toIndex); branches.moveBeforeBranch([ fromBranchId ], toBranchId); } } From bb0f384a39853193d1f30a8ab2a3b38e28905a25 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 20:30:36 +0300 Subject: [PATCH 033/107] feat(views/table): disable drag if sorted --- .../widgets/view_widgets/table_view/data.ts | 3 +- .../widgets/view_widgets/table_view/index.ts | 98 ++++++++++--------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 1ac64e364..a9c7ceb75 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -37,8 +37,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }, { field: "title", - editable: true, - rowDrag: true, + editable: true }, { field: "position" 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 024509970..6297bcde0 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -81,8 +81,8 @@ export default class TableView extends ViewMode { this.api = createGrid(el, { ...buildData(parentNote, info, notes), - ...setupEditing(), - ...setupDragging(), + ...this.setupEditing(), + ...this.setupDragging(), initialState, async onGridReady(event) { applyHeaderCustomization(el, event.api); @@ -101,56 +101,62 @@ export default class TableView extends ViewMode { }); } -} + private setupEditing(): GridOptions { + return { + onCellValueChanged(event) { + if (event.type !== "cellValueChanged") { + return; + } -function setupEditing(): GridOptions { - return { - onCellValueChanged(event) { - if (event.type !== "cellValueChanged") { - return; - } + const noteId = event.data.noteId; + const name = event.colDef.field; + if (!name) { + return; + } - const noteId = event.data.noteId; - const name = event.colDef.field; - if (!name) { - return; - } + const { newValue } = event; + if (name === "title") { + // TODO: Deduplicate with note_title. + server.put(`notes/${noteId}/title`, { title: newValue }); + } - const { newValue } = event; - if (name === "title") { - // TODO: Deduplicate with note_title. - server.put(`notes/${noteId}/title`, { title: newValue }); - } - - if (name.startsWith("labels.")) { - const labelName = name.split(".", 2)[1]; - setLabel(noteId, labelName, newValue); + if (name.startsWith("labels.")) { + const labelName = name.split(".", 2)[1]; + setLabel(noteId, labelName, newValue); + } } } } -} -function setupDragging() { - return { - onRowDragEnd(e) { - const fromIndex = e.node.rowIndex; - const toIndex = e.overNode?.rowIndex; - if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) { - return; - } - - const isBelow = (toIndex > fromIndex); - const fromBranchId = e.node.data?.branchId; - const toBranchId = e.overNode?.data?.branchId; - if (fromBranchId === undefined || toBranchId === undefined) { - return; - } - - if (isBelow) { - branches.moveAfterBranch([ fromBranchId ], toBranchId); - } else { - branches.moveBeforeBranch([ fromBranchId ], toBranchId); - } + private setupDragging() { + if (this.parentNote.hasLabel("sorted")) { + return {}; } - }; + + const config: GridOptions = { + rowDragEntireRow: true, + onRowDragEnd(e) { + const fromIndex = e.node.rowIndex; + const toIndex = e.overNode?.rowIndex; + if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) { + return; + } + + const isBelow = (toIndex > fromIndex); + const fromBranchId = e.node.data?.branchId; + const toBranchId = e.overNode?.data?.branchId; + if (fromBranchId === undefined || toBranchId === undefined) { + return; + } + + if (isBelow) { + branches.moveAfterBranch([ fromBranchId ], toBranchId); + } else { + branches.moveBeforeBranch([ fromBranchId ], toBranchId); + } + } + }; + return config; + } } + From f8e10f36db09905dac504486563b758f2d7d4c7c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 21:51:38 +0300 Subject: [PATCH 034/107] refactor(note_list): use object for constructor arg --- apps/client/src/services/note_list_renderer.ts | 10 ++-------- apps/client/src/widgets/note_list.ts | 6 +++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/client/src/services/note_list_renderer.ts b/apps/client/src/services/note_list_renderer.ts index 9518c3b79..3219d8d92 100644 --- a/apps/client/src/services/note_list_renderer.ts +++ b/apps/client/src/services/note_list_renderer.ts @@ -12,14 +12,8 @@ export default class NoteListRenderer { private viewType: ViewTypeOptions; public viewMode: ViewMode | null; - constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - this.viewType = this.#getViewType(parentNote); - const args: ViewModeArgs = { - $parent, - parentNote, - noteIds, - showNotePath - }; + constructor(args: ViewModeArgs) { + this.viewType = this.#getViewType(args.parentNote); switch (this.viewType) { case "list": diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index ba1a028f2..c49e4870c 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -76,7 +76,11 @@ export default class NoteListWidget extends NoteContextAwareWidget { } async renderNoteList(note: FNote) { - const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds()); + const noteListRenderer = new NoteListRenderer({ + $parent: this.$content, + parentNote: note, + noteIds: note.getChildNoteIds() + }); this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); await noteListRenderer.renderList(); this.viewMode = noteListRenderer.viewMode; From fe1dbb4cbf6cdffe95e1ce6877660f3228a9f3f6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 22:19:09 +0300 Subject: [PATCH 035/107] feat(views/table): display a dialog to add a new column --- apps/client/src/widgets/note_list.ts | 30 ++++++++++++++++++- .../widgets/view_widgets/table_view/index.ts | 17 +++++++++++ .../src/widgets/view_widgets/view_mode.ts | 5 +++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index c49e4870c..81a818bdb 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -1,8 +1,10 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteListRenderer from "../services/note_list_renderer.js"; import type FNote from "../entities/fnote.js"; -import type { CommandListener, CommandListenerData, EventData } from "../components/app_context.js"; +import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData } 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*/`
@@ -37,6 +39,14 @@ export default class NoteListWidget extends NoteContextAwareWidget { private noteIdRefreshed?: string; private shownNoteId?: string | null; private viewMode?: ViewMode | null; + private attributeDetailWidget: AttributeDetailWidget; + + constructor() { + super(); + this.attributeDetailWidget = new AttributeDetailWidget() + .contentSized() + .setParent(this); + } isEnabled() { return super.isEnabled() && this.noteContext?.hasNoteList(); @@ -46,6 +56,7 @@ 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) => { @@ -64,6 +75,23 @@ 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 6297bcde0..0b6641612 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,6 +8,7 @@ import server from "../../../services/server.js"; import type { GridApi, GridState } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; +import type { CommandListenerData } from "../../../components/app_context.js"; const TPL = /*html*/`
@@ -33,6 +34,10 @@ const TPL = /*html*/` } +
+ +
+
`; @@ -158,5 +163,17 @@ export default class TableView extends ViewMode { }; return config; } + + async saveAttributesCommand() { + console.log("Save attributes"); + } + + async reloadAttributesCommand() { + console.log("Reload attributes"); + } + + async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { + console.log("Update attributes", { attributes }); + } } diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index caa107da9..c3552e020 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -1,6 +1,8 @@ import type { EventData } 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"; +import type NoteListWidget from "../note_list.js"; import ViewModeStorage from "./view_mode_storage.js"; export interface ViewModeArgs { @@ -10,13 +12,14 @@ export interface ViewModeArgs { showNotePath?: boolean; } -export default abstract class ViewMode { +export default abstract class ViewMode extends Component { private _viewStorage: ViewModeStorage | null; protected parentNote: FNote; protected viewType: ViewTypeOptions; constructor(args: ViewModeArgs, viewType: ViewTypeOptions) { + super(); this.parentNote = args.parentNote; this._viewStorage = null; // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work From 0fb0be4ffceee423e7a9ec3549569b452f5cb581 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 22:43:29 +0300 Subject: [PATCH 036/107] feat(views/table): actually add attributes --- apps/client/src/components/component.ts | 6 +----- apps/client/src/services/attributes.ts | 5 +++-- apps/client/src/widgets/note_list.ts | 9 ++++++++ .../widgets/view_widgets/table_view/index.ts | 21 +++++++++++++------ apps/server/src/routes/api/attributes.ts | 3 ++- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/apps/client/src/components/component.ts b/apps/client/src/components/component.ts index e2897d1f3..8686a7bb9 100644 --- a/apps/client/src/components/component.ts +++ b/apps/client/src/components/component.ts @@ -93,11 +93,7 @@ export class TypedComponent> { if (fun) { return this.callMethod(fun, data); - } else { - if (!this.parent) { - throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`); - } - + } else if (this.parent) { return this.parent.triggerCommand(name, data); } } diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 315dc2c15..95fb75726 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -3,11 +3,12 @@ import froca from "./froca.js"; import type FNote from "../entities/fnote.js"; import type { AttributeRow } from "./load_results.js"; -async function addLabel(noteId: string, name: string, value: string = "") { +async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) { await server.put(`notes/${noteId}/attribute`, { type: "label", name: name, - value: value + value: value, + isInheritable }); } diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index 81a818bdb..150c4933e 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -166,4 +166,13 @@ export default class NoteListWidget extends NoteContextAwareWidget { } } + triggerCommand(name: K, data?: CommandMappings[K]): Promise | undefined | null { + // Pass the commands to the view mode, which is not actually attached to the hierarchy. + if (this.viewMode?.triggerCommand(name, data)) { + return; + } + + return super.triggerCommand(name, data); + } + } 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 0b6641612..7ec559a85 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,7 +1,7 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; -import { setLabel } from "../../../services/attributes.js"; +import attributes, { setLabel } from "../../../services/attributes.js"; import getPromotedAttributeInformation, { buildData, TableData } from "./data.js"; import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; @@ -9,6 +9,7 @@ import type { GridApi, GridState } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; import type { CommandListenerData } from "../../../components/app_context.js"; +import type { Attribute } from "../../../services/attribute_parser.js"; const TPL = /*html*/`
@@ -53,6 +54,7 @@ export default class TableView extends ViewMode { private args: ViewModeArgs; private spacedUpdate: SpacedUpdate; private api?: GridApi; + private newAttribute?: Attribute; constructor(args: ViewModeArgs) { super(args, "table"); @@ -164,16 +166,23 @@ export default class TableView extends ViewMode { return config; } - async saveAttributesCommand() { - console.log("Save attributes"); - } - async reloadAttributesCommand() { console.log("Reload attributes"); } async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) { - console.log("Update attributes", { attributes }); + 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); + } + } diff --git a/apps/server/src/routes/api/attributes.ts b/apps/server/src/routes/api/attributes.ts index 7d542ac16..fa106180b 100644 --- a/apps/server/src/routes/api/attributes.ts +++ b/apps/server/src/routes/api/attributes.ts @@ -48,7 +48,8 @@ function updateNoteAttribute(req: Request) { attribute = new BAttribute({ noteId: noteId, name: body.name, - type: body.type + type: body.type, + isInheritable: body.isInheritable }); } From 44ce6a5169dbc8d52e30db6e0d9f4966760dd50b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 22:50:27 +0300 Subject: [PATCH 037/107] feat(views/table): refresh on attribute change --- .../src/widgets/view_widgets/table_view/index.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 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 7ec559a85..45fe41d45 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import server from "../../../services/server.js"; import type { GridApi, GridState } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; -import type { CommandListenerData } from "../../../components/app_context.js"; +import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; const TPL = /*html*/` @@ -184,5 +184,17 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } + onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + // Refresh if promoted attributes get changed. + if (loadResults.getAttributeRows().find(attr => + attr.type === "label" && + attr.name?.startsWith("label:") && + attributes.isAffecting(attr, this.parentNote))) { + return true; + } + + return false; + } + } From c058673e33302223d18336bfdb4c229ad2b9fb77 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 23:01:15 +0300 Subject: [PATCH 038/107] feat(views/table): smooth column update --- .../widgets/view_widgets/table_view/index.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 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 45fe41d45..96d3c0e72 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -2,7 +2,7 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; import attributes, { setLabel } from "../../../services/attributes.js"; -import getPromotedAttributeInformation, { buildData, TableData } from "./data.js"; +import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, TableData } from "./data.js"; import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; import type { GridApi, GridState } from "ag-grid-community"; @@ -79,15 +79,10 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const { noteIds, parentNote } = this.args; - const notes = await froca.getNotes(noteIds); - - const info = getPromotedAttributeInformation(parentNote); const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - this.api = createGrid(el, { - ...buildData(parentNote, info, notes), + const options: GridOptions = { ...this.setupEditing(), ...this.setupDragging(), initialState, @@ -95,6 +90,18 @@ export default class TableView extends ViewMode { applyHeaderCustomization(el, event.api); }, onStateUpdated: () => this.spacedUpdate.scheduleUpdate() + } + + this.api = createGrid(el, options); + this.loadData(); + } + + private async loadData() { + const notes = await froca.getNotes(this.args.noteIds); + const info = getPromotedAttributeInformation(this.parentNote); + + this.api?.updateGridOptions({ + ...buildData(this.parentNote, info, notes) }); } @@ -190,7 +197,11 @@ export default class TableView extends ViewMode { attr.type === "label" && attr.name?.startsWith("label:") && attributes.isAffecting(attr, this.parentNote))) { - return true; + const info = getPromotedAttributeInformation(this.parentNote); + const columnDefs = buildColumnDefinitions(info); + this.api?.updateGridOptions({ + columnDefs + }) } return false; From d31ba39a91d74e41efd055ea6fc6dd70765949f7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 27 Jun 2025 23:40:00 +0300 Subject: [PATCH 039/107] feat(views/table): basic dark mode support --- .../src/widgets/view_widgets/table_view/index.ts | 13 +++++++++++-- 1 file changed, 11 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 96d3c0e72..98bc82d81 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,11 +1,11 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; -import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions } from "ag-grid-community"; +import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions, themeQuartz, colorSchemeDark } from "ag-grid-community"; import attributes, { setLabel } from "../../../services/attributes.js"; import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, TableData } from "./data.js"; import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; -import type { GridApi, GridState } from "ag-grid-community"; +import type { GridApi, GridState, Theme } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; @@ -86,6 +86,7 @@ export default class TableView extends ViewMode { ...this.setupEditing(), ...this.setupDragging(), initialState, + theme: this.getTheme(), async onGridReady(event) { applyHeaderCustomization(el, event.api); }, @@ -191,6 +192,14 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } + private getTheme(): Theme { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + return themeQuartz.withPart(colorSchemeDark) + } else { + return themeQuartz; + } + } + onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { // Refresh if promoted attributes get changed. if (loadResults.getAttributeRows().find(attr => From 7f2c41940d9c8e1c575eaa12bdfe4f0a7b06b43e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 00:07:14 +0300 Subject: [PATCH 040/107] feat(views/table): add basic row creation mechanism --- apps/client/src/widgets/note_list.ts | 1 + .../widgets/view_widgets/table_view/index.ts | 17 ++++++++++++++++- .../src/widgets/view_widgets/view_mode.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/note_list.ts b/apps/client/src/widgets/note_list.ts index 150c4933e..389aa82d3 100644 --- a/apps/client/src/widgets/note_list.ts +++ b/apps/client/src/widgets/note_list.ts @@ -107,6 +107,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { const noteListRenderer = new NoteListRenderer({ $parent: this.$content, parentNote: note, + parentNotePath: this.notePath, noteIds: note.getChildNoteIds() }); this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight); 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 98bc82d81..66af36255 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -2,7 +2,7 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions, themeQuartz, colorSchemeDark } from "ag-grid-community"; import attributes, { setLabel } from "../../../services/attributes.js"; -import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, TableData } from "./data.js"; +import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js"; import applyHeaderCustomization from "./header-customization.js"; import server from "../../../services/server.js"; import type { GridApi, GridState, Theme } from "ag-grid-community"; @@ -10,6 +10,7 @@ import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; +import note_create from "../../../services/note_create.js"; const TPL = /*html*/`
@@ -37,6 +38,7 @@ const TPL = /*html*/`
+
@@ -192,6 +194,15 @@ export default class TableView extends ViewMode { console.log("Save attributes", this.newAttribute); } + addNewRowCommand() { + const parentNotePath = this.args.parentNotePath; + if (parentNotePath) { + note_create.createNote(parentNotePath, { + activate: false + }); + } + } + private getTheme(): Theme { if (window.matchMedia('(prefers-color-scheme: dark)').matches) { return themeQuartz.withPart(colorSchemeDark) @@ -213,6 +224,10 @@ export default class TableView extends ViewMode { }) } + if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { + return true; + } + return false; } diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index c3552e020..f50c7841b 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -2,12 +2,12 @@ import type { EventData } 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"; -import type NoteListWidget from "../note_list.js"; import ViewModeStorage from "./view_mode_storage.js"; export interface ViewModeArgs { $parent: JQuery; parentNote: FNote; + parentNotePath: string | null | undefined; noteIds: string[]; showNotePath?: boolean; } From 4ef93569a158d5245250521d251aef79014c1391 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 12:00:50 +0300 Subject: [PATCH 041/107] refactor(views/table): start switching to tabulator --- apps/client/package.json | 2 +- .../widgets/view_widgets/table_view/index.ts | 24 ++++--------------- pnpm-lock.yaml | 23 +++++++----------- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 8f1ba4de7..d267b8565 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -26,7 +26,6 @@ "@triliumnext/commons": "workspace:*", "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", - "ag-grid-community": "33.3.2", "autocomplete.js": "0.38.1", "bootstrap": "5.3.7", "boxicons": "2.1.4", @@ -55,6 +54,7 @@ "preact": "10.26.9", "split.js": "1.6.5", "svg-pan-zoom": "3.6.2", + "tabulator-tables": "6.3.1", "vanilla-js-wheel-zoom": "9.0.4" }, "devDependencies": { 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 66af36255..f849f3a65 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,16 +1,15 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; -import { createGrid, AllCommunityModule, ModuleRegistry, GridOptions, themeQuartz, colorSchemeDark } from "ag-grid-community"; import attributes, { setLabel } from "../../../services/attributes.js"; -import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js"; -import applyHeaderCustomization from "./header-customization.js"; +import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, TableData } from "./data.js"; import server from "../../../services/server.js"; -import type { GridApi, GridState, Theme } from "ag-grid-community"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; +import {Tabulator} from 'tabulator-tables'; +import "tabulator-tables/dist/css/tabulator.min.css"; const TPL = /*html*/`
@@ -66,8 +65,6 @@ export default class TableView extends ViewMode { this.args = args; this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); args.$parent.append(this.$root); - - ModuleRegistry.registerModules([ AllCommunityModule ]); } get isFullHeight(): boolean { @@ -84,19 +81,8 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const options: GridOptions = { - ...this.setupEditing(), - ...this.setupDragging(), - initialState, - theme: this.getTheme(), - async onGridReady(event) { - applyHeaderCustomization(el, event.api); - }, - onStateUpdated: () => this.spacedUpdate.scheduleUpdate() - } - - this.api = createGrid(el, options); - this.loadData(); + const table = new Tabulator(el, { + }); } private async loadData() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db3bd8690..b92fb2cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,9 +202,6 @@ importers: '@triliumnext/share-theme': specifier: workspace:* version: link:../../packages/share-theme - ag-grid-community: - specifier: 33.3.2 - version: 33.3.2 autocomplete.js: specifier: 0.38.1 version: 0.38.1 @@ -289,6 +286,9 @@ importers: svg-pan-zoom: specifier: 3.6.2 version: 3.6.2 + tabulator-tables: + specifier: 6.3.1 + version: 6.3.1 vanilla-js-wheel-zoom: specifier: 9.0.4 version: 9.0.4 @@ -5989,12 +5989,6 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - ag-charts-types@11.3.2: - resolution: {integrity: sha512-trPGqgGYiTeLgtf9nLuztDYOPOFOLbqHn1g2D99phf7QowcwdX0TPx0wfWG8Hm90LjB8IH+G2s3AZe2vrdAtMQ==} - - ag-grid-community@33.3.2: - resolution: {integrity: sha512-9bx0e/+ykOyLvUxHqmdy0cRVANH6JAtv0yZdnBZEXYYqBAwN+G5a4NY+2I1KvoOCYzbk8SnStG7y4hCdVAAWOQ==} - agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -13023,6 +13017,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tabulator-tables@6.3.1: + resolution: {integrity: sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==} + tailwindcss@4.1.10: resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==} @@ -21137,12 +21134,6 @@ snapshots: address@1.2.2: {} - ag-charts-types@11.3.2: {} - - ag-grid-community@33.3.2: - dependencies: - ag-charts-types: 11.3.2 - agent-base@6.0.2: dependencies: debug: 4.4.1(supports-color@6.0.0) @@ -29465,6 +29456,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tabulator-tables@6.3.1: {} + tailwindcss@4.1.10: {} tapable@2.2.1: {} From 16b9375b9d3d386909169a1084342d2be8742f0f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 12:18:17 +0300 Subject: [PATCH 042/107] chore(views/table): add types --- apps/client/package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/apps/client/package.json b/apps/client/package.json index d267b8565..ae49e4a03 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -64,6 +64,7 @@ "@types/leaflet": "1.9.19", "@types/leaflet-gpx": "1.3.7", "@types/mark.js": "8.11.12", + "@types/tabulator-tables": "6.2.6", "copy-webpack-plugin": "13.0.0", "happy-dom": "18.0.1", "script-loader": "0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b92fb2cdc..8a1268fcf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,6 +311,9 @@ importers: '@types/mark.js': specifier: 8.11.12 version: 8.11.12 + '@types/tabulator-tables': + specifier: 6.2.6 + version: 6.2.6 copy-webpack-plugin: specifier: 13.0.0 version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.5)) @@ -5460,6 +5463,9 @@ packages: '@types/swagger-ui@5.21.1': resolution: {integrity: sha512-DUmUH59eeOtvAqcWwBduH2ws0cc5i95KHsXCS4FsOfbUq/clW8TN+HqRBj7q5p9MSsSNK43RziIGItNbrAGLxg==} + '@types/tabulator-tables@6.2.6': + resolution: {integrity: sha512-A+2VrqDluI6hNw5dQl1Z7b8pjQfAE62+3Kj0cFfenWzj0T0ewMicPrpPINHL7ASqz9u9FTDn1Mz1Ige2tF4Wlw==} + '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} @@ -20310,6 +20316,8 @@ snapshots: '@types/swagger-ui@5.21.1': {} + '@types/tabulator-tables@6.2.6': {} + '@types/tmp@0.2.6': {} '@types/tough-cookie@4.0.5': {} From 30f793961692488c308a9e9245b8c751feca45a0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 12:18:24 +0300 Subject: [PATCH 043/107] chore(views/table): reintroduce column definitions --- .../src/widgets/view_widgets/table_view/data.ts | 17 +++++++++-------- .../widgets/view_widgets/table_view/index.ts | 9 ++------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index a9c7ceb75..ef76c477b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,7 +1,8 @@ -import { GridOptions } from "ag-grid-community"; import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; import froca from "../../../services/froca.js"; +import { title } from "process"; +import type { ColumnDefinition } from "tabulator-tables"; export type TableData = { noteId: string; @@ -30,26 +31,26 @@ export function buildData(parentNote: FNote, info: PromotedAttributeInformation[ } export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { - const columnDefs: GridOptions["columnDefs"] = [ + const columnDefs: ColumnDefinition[] = [ { field: "noteId", - editable: false + title: "Note ID", }, { field: "title", - editable: true + title: "Title" }, { - field: "position" + field: "position", + title: "Position" } ]; for (const { name, title, type } of info) { columnDefs.push({ field: `labels.${name}`, - headerName: title, - cellDataType: mapDataType(type), - editable: true + title: title ?? name, + // cellDataType: mapDataType(type), }); } 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 f849f3a65..26f5b9ee7 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -81,16 +81,11 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const table = new Tabulator(el, { - }); - } - - private async loadData() { const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); - this.api?.updateGridOptions({ - ...buildData(this.parentNote, info, notes) + const table = new Tabulator(el, { + columns: buildColumnDefinitions(info) }); } From e09a7fb6e09fbe024018b6b08f5ecb784f73669c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 12:24:40 +0300 Subject: [PATCH 044/107] chore(views/table): reintroduce rows --- .../widgets/view_widgets/table_view/index.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/index.ts b/apps/client/src/widgets/view_widgets/table_view/index.ts index 26f5b9ee7..3017ebfb1 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,7 +1,7 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; import attributes, { setLabel } from "../../../services/attributes.js"; -import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, TableData } from "./data.js"; +import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js"; import server from "../../../services/server.js"; import SpacedUpdate from "../../../services/spaced_update.js"; import branches from "../../../services/branches.js"; @@ -54,7 +54,7 @@ export default class TableView extends ViewMode { private $container: JQuery; private args: ViewModeArgs; private spacedUpdate: SpacedUpdate; - private api?: GridApi; + private api?: Tabulator; private newAttribute?: Attribute; constructor(args: ViewModeArgs) { @@ -81,12 +81,21 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; + this.api = new Tabulator(el, { + }); + this.loadData(); + } + + private async loadData() { + if (!this.api) { + return; + } + const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); - const table = new Tabulator(el, { - columns: buildColumnDefinitions(info) - }); + this.api.setColumns(buildColumnDefinitions(info)); + this.api.setData(buildRowDefinitions(this.parentNote, notes, info)); } private onSave() { From f528fa25d1772ea44039868a6e57f00e25c351a2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 12:51:19 +0300 Subject: [PATCH 045/107] feat(views/table): switch to bootstrap theme --- 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 3017ebfb1..f7133458a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -9,7 +9,7 @@ import type { CommandListenerData, EventData } from "../../../components/app_con import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; import {Tabulator} from 'tabulator-tables'; -import "tabulator-tables/dist/css/tabulator.min.css"; +import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/`
From b2d20af51a8c1095a56ed6088ac9a1585ee8ae22 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 16:39:24 +0300 Subject: [PATCH 046/107] fix(views/table): refreshing of columns --- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 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 f7133458a..1d299c9c0 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -209,9 +209,7 @@ export default class TableView extends ViewMode { attributes.isAffecting(attr, this.parentNote))) { const info = getPromotedAttributeInformation(this.parentNote); const columnDefs = buildColumnDefinitions(info); - this.api?.updateGridOptions({ - columnDefs - }) + this.api?.setColumns(columnDefs) } if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { From ada39cd3c7ccf92a73f35748bdde8cffad65bda6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 16:48:01 +0300 Subject: [PATCH 047/107] fix(views/table): error when adding a new row --- .../src/widgets/view_widgets/table_view/data.ts | 12 ++++++++---- .../src/widgets/view_widgets/table_view/index.ts | 14 ++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index ef76c477b..1eb6faebd 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -20,9 +20,9 @@ export interface PromotedAttributeInformation { type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; -export function buildData(parentNote: FNote, info: PromotedAttributeInformation[], notes: FNote[]) { +export async function buildData(parentNote: FNote, info: PromotedAttributeInformation[], notes: FNote[]) { const columnDefs = buildColumnDefinitions(info); - const rowData = buildRowDefinitions(parentNote, notes, info); + const rowData = await buildRowDefinitions(parentNote, notes, info); return { rowData, @@ -75,10 +75,14 @@ function mapDataType(labelType: LabelType | undefined): GridLabelType { } } -export function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { +export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { const definitions: GridOptions["rowData"] = []; for (const branch of parentNote.getChildBranches()) { - const note = branch.getNoteFromCache(); + const note = await branch.getNote(); + if (!note) { + continue; // Skip if the note is not found + } + const labels: typeof definitions[0]["labels"] = {}; for (const { name, type } of infos) { if (type === "boolean") { 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 1d299c9c0..399de4724 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -95,7 +95,7 @@ export default class TableView extends ViewMode { const info = getPromotedAttributeInformation(this.parentNote); this.api.setColumns(buildColumnDefinitions(info)); - this.api.setData(buildRowDefinitions(this.parentNote, notes, info)); + this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); } private onSave() { @@ -201,7 +201,11 @@ export default class TableView extends ViewMode { } } - onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + if (!this.api) { + return; + } + // Refresh if promoted attributes get changed. if (loadResults.getAttributeRows().find(attr => attr.type === "label" && @@ -209,11 +213,13 @@ export default class TableView extends ViewMode { attributes.isAffecting(attr, this.parentNote))) { const info = getPromotedAttributeInformation(this.parentNote); const columnDefs = buildColumnDefinitions(info); - this.api?.setColumns(columnDefs) + this.api.setColumns(columnDefs) } if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { - return true; + const notes = await froca.getNotes(this.args.noteIds); + const info = getPromotedAttributeInformation(this.parentNote); + this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); } return false; From 50ebcd552cf58beb7ec83f715b66238937f7cc57 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 16:51:24 +0300 Subject: [PATCH 048/107] fix(views/table): error when adding a new column --- .../widgets/view_widgets/table_view/index.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 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 399de4724..c410a8717 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -201,7 +201,7 @@ export default class TableView extends ViewMode { } } - async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { + onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void { if (!this.api) { return; } @@ -211,19 +211,27 @@ export default class TableView extends ViewMode { attr.type === "label" && attr.name?.startsWith("label:") && attributes.isAffecting(attr, this.parentNote))) { - const info = getPromotedAttributeInformation(this.parentNote); - const columnDefs = buildColumnDefinitions(info); - this.api.setColumns(columnDefs) + this.#manageColumnUpdate(); } if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) { - const notes = await froca.getNotes(this.args.noteIds); - const info = getPromotedAttributeInformation(this.parentNote); - this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); + this.#manageRowsUpdate(); } return false; } + #manageColumnUpdate() { + const info = getPromotedAttributeInformation(this.parentNote); + const columnDefs = buildColumnDefinitions(info); + this.api.setColumns(columnDefs); + } + + async #manageRowsUpdate() { + const notes = await froca.getNotes(this.args.noteIds); + const info = getPromotedAttributeInformation(this.parentNote); + this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); + } + } From 8e51469de5c5d80e133f27cb282a0f38d64f3b6c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 16:56:36 +0300 Subject: [PATCH 049/107] chore(views/table): re-enable sorting --- apps/client/src/widgets/view_widgets/table_view/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 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 c410a8717..2c9fe484e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator} from 'tabulator-tables'; +import {Tabulator, SortModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -81,8 +81,8 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - this.api = new Tabulator(el, { - }); + Tabulator.registerModule(SortModule); + this.api = new Tabulator(el, {}); this.loadData(); } From 4a26f30d65ab6b95335d88058211181dd0adca8a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 17:07:11 +0300 Subject: [PATCH 050/107] feat(views/table): render note icon --- .../src/widgets/view_widgets/table_view/data.ts | 13 +++++++++++++ .../src/widgets/view_widgets/table_view/index.ts | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 1eb6faebd..c9034607f 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -32,6 +32,18 @@ export async function buildData(parentNote: FNote, info: PromotedAttributeInform export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { const columnDefs: ColumnDefinition[] = [ + { + field: "iconClass", + title: "Icon", + width: 40, + headerSort: false, + hozAlign: "center", + formatter(cell) { + console.log(cell); + const iconClass = cell.getValue(); + return ``; + }, + }, { field: "noteId", title: "Note ID", @@ -92,6 +104,7 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf } } definitions.push({ + iconClass: note.getIcon(), noteId: note.noteId, title: note.title, labels, 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 2c9fe484e..0eb795fa8 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -81,7 +81,11 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - Tabulator.registerModule(SortModule); + const modules = [SortModule, FormatModule]; + for (const module of modules) { + Tabulator.registerModule(module); + } + this.api = new Tabulator(el, {}); this.loadData(); } From 56d366a286418fbcc458221c439302e95edf26f1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 17:23:42 +0300 Subject: [PATCH 051/107] feat(views/table): add column to open note --- .../widgets/view_widgets/table_view/data.ts | 19 ++++++++++++++++--- .../widgets/view_widgets/table_view/index.ts | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index c9034607f..8c5852808 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,8 +1,7 @@ import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; -import froca from "../../../services/froca.js"; -import { title } from "process"; import type { ColumnDefinition } from "tabulator-tables"; +import link from "../../../services/link.js"; export type TableData = { noteId: string; @@ -39,7 +38,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { headerSort: false, hozAlign: "center", formatter(cell) { - console.log(cell); const iconClass = cell.getValue(); return ``; }, @@ -66,6 +64,21 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }); } + // End actions + columnDefs.push({ + title: "Open note", + width: 40, + hozAlign: "center", + formatter: () => ``, + cellClick: (e, cell) => { + const noteId = cell.getRow().getCell("noteId").getValue(); + console.log("Got note ID", noteId); + if (noteId) { + link.goToLinkExt(e as MouseEvent, `#root/${noteId}`); + } + } + }); + return columnDefs; } 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 0eb795fa8..017a595cd 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -81,7 +81,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule]; + const modules = [SortModule, FormatModule, InteractionModule]; for (const module of modules) { Tabulator.registerModule(module); } From 3d2db23f330e424286c70b481a8c411b5725f02e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 17:24:18 +0300 Subject: [PATCH 052/107] fix(views/table): use a more stable loading mechanism --- .../widgets/view_widgets/table_view/index.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 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 017a595cd..b413f60bc 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -86,20 +86,17 @@ export default class TableView extends ViewMode { Tabulator.registerModule(module); } - this.api = new Tabulator(el, {}); - this.loadData(); + this.initialize(el); } - private async loadData() { - if (!this.api) { - return; - } - + private async initialize(el: HTMLElement) { const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); - this.api.setColumns(buildColumnDefinitions(info)); - this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); + this.api = new Tabulator(el, { + columns: buildColumnDefinitions(info), + data: await buildRowDefinitions(this.parentNote, notes, info) + }); } private onSave() { From bc36676fa1484c70003927f0d4bf798deb5d6c7a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 17:45:11 +0300 Subject: [PATCH 053/107] chore(views/table): disable sorting for note action button --- apps/client/src/widgets/view_widgets/table_view/data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 8c5852808..c31df449a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -69,6 +69,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { title: "Open note", width: 40, hozAlign: "center", + headerSort: false, formatter: () => ``, cellClick: (e, cell) => { const noteId = cell.getRow().getCell("noteId").getValue(); From 0e27cd0801aea720c3ad80eb034d23f5e1e91d05 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 22:47:49 +0300 Subject: [PATCH 054/107] feat(views/table): add row number --- apps/client/src/widgets/view_widgets/table_view/data.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index c31df449a..d9111ffee 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -31,6 +31,14 @@ export async function buildData(parentNote: FNote, info: PromotedAttributeInform export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { const columnDefs: ColumnDefinition[] = [ + { + title: "#", + formatter: "rownum", + headerSort: false, + hozAlign: "center", + resizable: false, + frozen: true + }, { field: "iconClass", title: "Icon", From a31ac17792f4f6282209734f4dd64776d10746b0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 22:49:40 +0300 Subject: [PATCH 055/107] chore(views/table): set row ID as index --- 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 b413f60bc..3267e96c5 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -94,6 +94,7 @@ export default class TableView extends ViewMode { const info = getPromotedAttributeInformation(this.parentNote); this.api = new Tabulator(el, { + index: "noteId", columns: buildColumnDefinitions(info), data: await buildRowDefinitions(this.parentNote, notes, info) }); From 9a6a8580de8ca9995040332e082c7d951034421b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 23:19:54 +0300 Subject: [PATCH 056/107] chore(views/table): bring back editing title --- .../widgets/view_widgets/table_view/data.ts | 4 +- .../widgets/view_widgets/table_view/index.ts | 58 ++++++++++--------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index d9111ffee..d798b345e 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -56,7 +56,8 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }, { field: "title", - title: "Title" + title: "Title", + editor: "input" }, { field: "position", @@ -68,6 +69,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { columnDefs.push({ field: `labels.${name}`, title: title ?? name, + editable: true // cellDataType: mapDataType(type), }); } 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 3267e96c5..53bc5da8a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,8 +8,8 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule} from 'tabulator-tables'; -import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule} from 'tabulator-tables'; +import "tabulator-tables/dist/css/tabulator_midnight.min.css"; const TPL = /*html*/`
@@ -81,7 +81,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule, InteractionModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule]; for (const module of modules) { Tabulator.registerModule(module); } @@ -98,6 +98,7 @@ export default class TableView extends ViewMode { columns: buildColumnDefinitions(info), data: await buildRowDefinitions(this.parentNote, notes, info) }); + this.setupEditing(); } private onSave() { @@ -110,31 +111,36 @@ export default class TableView extends ViewMode { }); } - private setupEditing(): GridOptions { - return { - onCellValueChanged(event) { - if (event.type !== "cellValueChanged") { - return; - } + private setupEditing() { + this.api!.on("cellEdited", (cell) => { + const noteId = cell.getRow().getData().noteId; + const field = cell.getField(); + const newValue = cell.getValue(); - const noteId = event.data.noteId; - const name = event.colDef.field; - if (!name) { - return; - } - - const { newValue } = event; - if (name === "title") { - // TODO: Deduplicate with note_title. - server.put(`notes/${noteId}/title`, { title: newValue }); - } - - if (name.startsWith("labels.")) { - const labelName = name.split(".", 2)[1]; - setLabel(noteId, labelName, newValue); - } + console.log("Cell edited", field, newValue); + if (field === "title") { + server.put(`notes/${noteId}/title`, { title: newValue }); } - } + }); + + // return { + // onCellValueChanged(event) { + // if (event.type !== "cellValueChanged") { + // return; + // } + + // const noteId = event.data.noteId; + // const name = event.colDef.field; + // if (!name) { + // return; + // } + + // if (name.startsWith("labels.")) { + // const labelName = name.split(".", 2)[1]; + // setLabel(noteId, labelName, newValue); + // } + // } + // } } private setupDragging() { From 09b800b9ade55067028cfce855fbe5f26c310672 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 23:23:29 +0300 Subject: [PATCH 057/107] chore(views/table): bring back editing attributes --- .../widgets/view_widgets/table_view/data.ts | 2 +- .../widgets/view_widgets/table_view/index.ts | 26 +++++-------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index d798b345e..d89a449f6 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -69,7 +69,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { columnDefs.push({ field: `labels.${name}`, title: title ?? name, - editable: true + editor: "input" // cellDataType: mapDataType(type), }); } 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 53bc5da8a..f06592175 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -117,30 +117,16 @@ export default class TableView extends ViewMode { const field = cell.getField(); const newValue = cell.getValue(); - console.log("Cell edited", field, newValue); if (field === "title") { server.put(`notes/${noteId}/title`, { title: newValue }); + return; + } + + if (field.startsWith("labels.")) { + const labelName = field.split(".", 2)[1]; + setLabel(noteId, labelName, newValue); } }); - - // return { - // onCellValueChanged(event) { - // if (event.type !== "cellValueChanged") { - // return; - // } - - // const noteId = event.data.noteId; - // const name = event.colDef.field; - // if (!name) { - // return; - // } - - // if (name.startsWith("labels.")) { - // const labelName = name.split(".", 2)[1]; - // setLabel(noteId, labelName, newValue); - // } - // } - // } } private setupDragging() { From e7ca56e061e8641b4de36386bb6b03bd17d316d8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 23:29:31 +0300 Subject: [PATCH 058/107] chore(views/table): support more data types --- .../widgets/view_widgets/table_view/data.ts | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index d89a449f6..b09cc1fec 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -17,6 +17,35 @@ export interface PromotedAttributeInformation { type?: LabelType; } +const labelTypeMappings: Record> = { + text: { + editor: "input" + }, + boolean: { + formatter: "tickCross", + editor: "tickCross" + }, + date: { + formatter: "datetime", + editor: "date", + }, + datetime: { + formatter: "datetime", + editor: "datetime" + }, + number: { + editor: "number" + }, + time: { + formatter: "datetime", + editor: "datetime" + }, + url: { + formatter: "link", + editor: "input" + } +}; + type GridLabelType = 'text' | 'number' | 'boolean' | 'date' | 'dateString' | 'object'; export async function buildData(parentNote: FNote, info: PromotedAttributeInformation[], notes: FNote[]) { @@ -69,8 +98,8 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { columnDefs.push({ field: `labels.${name}`, title: title ?? name, - editor: "input" - // cellDataType: mapDataType(type), + editor: "input", + ...labelTypeMappings[type ?? "text"], }); } @@ -93,24 +122,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { return columnDefs; } -function mapDataType(labelType: LabelType | undefined): GridLabelType { - if (!labelType) { - return "text"; - } - - switch (labelType) { - case "number": - return "number"; - case "boolean": - return "boolean"; - case "date": - return "dateString"; - case "text": - default: - return "text" - } -} - export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { const definitions: GridOptions["rowData"] = []; for (const branch of parentNote.getChildBranches()) { From dcea4c30ef1687645757d4cca0343c09014b9a4c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 23:33:52 +0300 Subject: [PATCH 059/107] chore(views/table): improve editing for date types --- apps/client/src/widgets/view_widgets/table_view/data.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index b09cc1fec..285187b13 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -26,19 +26,16 @@ const labelTypeMappings: Record> = { editor: "tickCross" }, date: { - formatter: "datetime", editor: "date", }, datetime: { - formatter: "datetime", editor: "datetime" }, number: { editor: "number" }, time: { - formatter: "datetime", - editor: "datetime" + editor: "input" }, url: { formatter: "link", From 8ee12f2950b1eb763d3032b7403ec8987215d0ab Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Jun 2025 23:50:54 +0300 Subject: [PATCH 060/107] chore(views/table): bring back resizing columns --- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 ++-- 1 file changed, 2 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 f06592175..13fd1dd4d 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_midnight.min.css"; const TPL = /*html*/` @@ -81,7 +81,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule, InteractionModule, EditModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule]; for (const module of modules) { Tabulator.registerModule(module); } From cf322b5c2a8506553838f207523d32164ed41c07 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 10:09:39 +0300 Subject: [PATCH 061/107] chore(views/table): back to bootstrap5 theme --- 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 13fd1dd4d..56bc29e10 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -9,7 +9,7 @@ import type { CommandListenerData, EventData } from "../../../components/app_con import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule} from 'tabulator-tables'; -import "tabulator-tables/dist/css/tabulator_midnight.min.css"; +import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/`
From a114fba06221207ed29364ae9dad6263eff2acd3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 15:11:09 +0300 Subject: [PATCH 062/107] chore(views/table): set up frozen columns --- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 ++-- 1 file changed, 2 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 56bc29e10..6d01e63f0 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -81,7 +81,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule]; for (const module of modules) { Tabulator.registerModule(module); } From 727eeb6c74a6a745269f8ae8cc1e89e6ec7a2f00 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 16:08:27 +0300 Subject: [PATCH 063/107] chore(views/table): bring back persistence --- .../widgets/view_widgets/table_view/index.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 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 6d01e63f0..b0e31e204 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -45,7 +45,7 @@ const TPL = /*html*/` `; export interface StateInfo { - gridState: GridState; + tableData: Record; } export default class TableView extends ViewMode { @@ -56,6 +56,7 @@ export default class TableView extends ViewMode { private spacedUpdate: SpacedUpdate; private api?: Tabulator; private newAttribute?: Attribute; + private persistentData: Record; constructor(args: ViewModeArgs) { super(args, "table"); @@ -64,6 +65,7 @@ export default class TableView extends ViewMode { 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); } @@ -81,7 +83,7 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule]; for (const module of modules) { Tabulator.registerModule(module); } @@ -96,18 +98,19 @@ export default class TableView extends ViewMode { this.api = new Tabulator(el, { index: "noteId", columns: buildColumnDefinitions(info), - data: await buildRowDefinitions(this.parentNote, notes, info) + data: await buildRowDefinitions(this.parentNote, notes, info), + persistence: true, + persistenceWriterFunc: (_id, type: string, data: object) => { + this.persistentData[type] = data; + this.spacedUpdate.scheduleUpdate(); + } }); this.setupEditing(); } private onSave() { - if (!this.api) { - return; - } - this.viewStorage.store({ - gridState: this.api.getState() + tableData: this.persistentData, }); } From 51b462f04319e06fddb13c5cdbc60d80ab045f2c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 16:16:15 +0300 Subject: [PATCH 064/107] chore(views/table): bring back restore state --- apps/client/src/widgets/view_widgets/table_view/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 b0e31e204..bdaab52bf 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -80,9 +80,6 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const viewStorage = await this.viewStorage.restore(); - const initialState = viewStorage?.gridState; - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule]; for (const module of modules) { Tabulator.registerModule(module); @@ -95,6 +92,8 @@ export default class TableView extends ViewMode { const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); + const viewStorage = await this.viewStorage.restore(); + this.persistentData = viewStorage?.tableData || {}; this.api = new Tabulator(el, { index: "noteId", columns: buildColumnDefinitions(info), @@ -103,7 +102,8 @@ export default class TableView extends ViewMode { persistenceWriterFunc: (_id, type: string, data: object) => { this.persistentData[type] = data; this.spacedUpdate.scheduleUpdate(); - } + }, + persistenceReaderFunc: (_id, type: string) => this.persistentData[type], }); this.setupEditing(); } From cedf91ea1a15c59e2206f47cac69fe793714aa53 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 16:56:34 +0300 Subject: [PATCH 065/107] chore(views/table): reintroduce column reordering --- apps/client/src/widgets/view_widgets/table_view/index.ts | 5 +++-- 1 file changed, 3 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 bdaab52bf..dff53c518 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,7 +8,7 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; const TPL = /*html*/` @@ -80,7 +80,7 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule]; for (const module of modules) { Tabulator.registerModule(module); } @@ -99,6 +99,7 @@ export default class TableView extends ViewMode { columns: buildColumnDefinitions(info), data: await buildRowDefinitions(this.parentNote, notes, info), persistence: true, + movableColumns: true, persistenceWriterFunc: (_id, type: string, data: object) => { this.persistentData[type] = data; this.spacedUpdate.scheduleUpdate(); From c5cc1fcc1ea1cb1f9cd5072e7c1f8bebab37e614 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Jun 2025 22:26:25 +0300 Subject: [PATCH 066/107] feat(views/table): introduce hiding of columns --- .../view_widgets/table_view/header-menu.ts | 51 +++++++++++++++++++ .../widgets/view_widgets/table_view/index.ts | 10 ++-- 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/header-menu.ts 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 new file mode 100644 index 000000000..9397757b4 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/header-menu.ts @@ -0,0 +1,51 @@ +export function applyHeaderMenu(columns) { + //apply header menu to each column + for(let column of columns){ + column.headerMenu = headerMenu; + } +} + +function headerMenu(){ + var menu = []; + var 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 dff53c518..7f731ce8c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,8 +8,9 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; +import { applyHeaderMenu } from "./header-menu.js"; const TPL = /*html*/`
@@ -80,7 +81,7 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule]; for (const module of modules) { Tabulator.registerModule(module); } @@ -92,11 +93,14 @@ export default class TableView extends ViewMode { const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); + const columnDefs = buildColumnDefinitions(info); + applyHeaderMenu(columnDefs); + const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; this.api = new Tabulator(el, { index: "noteId", - columns: buildColumnDefinitions(info), + columns: columnDefs, data: await buildRowDefinitions(this.parentNote, notes, info), persistence: true, movableColumns: true, From 84db4ed57ca9233ecc48e41db5c7377378f34a18 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 1 Jul 2025 11:56:05 +0300 Subject: [PATCH 067/107] docs(release): fix link --- docs/Release Notes/Release Notes/v0.96.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Release Notes/Release Notes/v0.96.0.md b/docs/Release Notes/Release Notes/v0.96.0.md index eb4ebdacc..1b2613487 100644 --- a/docs/Release Notes/Release Notes/v0.96.0.md +++ b/docs/Release Notes/Release Notes/v0.96.0.md @@ -6,7 +6,7 @@ > [!IMPORTANT] > If you enjoyed this release, consider showing a token of appreciation by: > -> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Notes) (top-right). +> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right). > * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran). ## 💡 Key highlights From 2cbb49681aaea8cc2278805b263e2170070b922b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 1 Jul 2025 12:09:13 +0300 Subject: [PATCH 068/107] fix(view/table): most type errors --- .../widgets/floating_buttons/help_button.ts | 3 ++- apps/client/src/widgets/search_result.ts | 8 ++++++- .../widgets/view_widgets/table_view/data.ts | 3 ++- .../table_view/header-customization.ts | 4 ++-- .../view_widgets/table_view/header-menu.ts | 23 ++++++++++--------- .../widgets/view_widgets/table_view/index.ts | 8 +++++++ .../src/widgets/view_widgets/view_mode.ts | 2 +- 7 files changed, 34 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/floating_buttons/help_button.ts b/apps/client/src/widgets/floating_buttons/help_button.ts index a68cb5711..3102ee59a 100644 --- a/apps/client/src/widgets/floating_buttons/help_button.ts +++ b/apps/client/src/widgets/floating_buttons/help_button.ts @@ -34,7 +34,8 @@ export const byNoteType: Record, string | null> = { export const byBookType: Record = { list: null, grid: null, - calendar: "xWbu3jpNWapp" + calendar: "xWbu3jpNWapp", + table: null }; export default class ContextualHelpButton extends NoteContextAwareWidget { diff --git a/apps/client/src/widgets/search_result.ts b/apps/client/src/widgets/search_result.ts index 155c3d2b6..09b199ccf 100644 --- a/apps/client/src/widgets/search_result.ts +++ b/apps/client/src/widgets/search_result.ts @@ -65,7 +65,13 @@ export default class SearchResultWidget extends NoteContextAwareWidget { return; } - const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true); + // this.$content, note, note.getChildNoteIds(), true + const noteListRenderer = new NoteListRenderer({ + $parent: this.$content, + parentNote: note, + noteIds: note.getChildNoteIds(), + showNotePath: true + }); await noteListRenderer.renderList(); } diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 285187b13..3ad275821 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -4,6 +4,7 @@ import type { ColumnDefinition } from "tabulator-tables"; import link from "../../../services/link.js"; export type TableData = { + iconClass: string; noteId: string; title: string; labels: Record; @@ -120,7 +121,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { } export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) { - const definitions: GridOptions["rowData"] = []; + const definitions: TableData[] = []; for (const branch of parentNote.getChildBranches()) { const note = await branch.getNote(); if (!note) { diff --git a/apps/client/src/widgets/view_widgets/table_view/header-customization.ts b/apps/client/src/widgets/view_widgets/table_view/header-customization.ts index 6907a3664..46484895f 100644 --- a/apps/client/src/widgets/view_widgets/table_view/header-customization.ts +++ b/apps/client/src/widgets/view_widgets/table_view/header-customization.ts @@ -8,10 +8,10 @@ export default function applyHeaderCustomization(baseEl: HTMLElement, api: GridA return; } - header.addEventListener("contextmenu", (e) => { + header.addEventListener("contextmenu", (_e) => { + const e = _e as MouseEvent; e.preventDefault(); - contextMenu.show({ items: [ { 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 index 9397757b4..4f53fbbe4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/header-menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/header-menu.ts @@ -1,16 +1,17 @@ +import type { CellComponent, MenuObject, Tabulator } from "tabulator-tables"; + export function applyHeaderMenu(columns) { //apply header menu to each column - for(let column of columns){ + for (let column of columns) { column.headerMenu = headerMenu; } } -function headerMenu(){ - var menu = []; - var columns = this.getColumns(); - - for(let column of columns){ +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"); @@ -27,8 +28,8 @@ function headerMenu(){ //create menu item menu.push({ - label:label, - action:function(e){ + label: label, + action: function (e) { //prevent menu closing e.stopPropagation(); @@ -36,10 +37,10 @@ function headerMenu(){ column.toggle(); //change menu item icon - if(column.isVisible()){ + if (column.isVisible()) { icon.classList.remove("bx-empty"); icon.classList.add("bx-check"); - }else{ + } else { icon.classList.remove("bx-check"); icon.classList.add("bx-empty"); } @@ -47,5 +48,5 @@ function headerMenu(){ }); } - return menu; + 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 7f731ce8c..5a06384cb 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -224,12 +224,20 @@ export default class TableView extends ViewMode { } #manageColumnUpdate() { + if (!this.api) { + return; + } + const info = getPromotedAttributeInformation(this.parentNote); const columnDefs = buildColumnDefinitions(info); this.api.setColumns(columnDefs); } async #manageRowsUpdate() { + if (!this.api) { + return; + } + const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); diff --git a/apps/client/src/widgets/view_widgets/view_mode.ts b/apps/client/src/widgets/view_widgets/view_mode.ts index f50c7841b..350b84dcb 100644 --- a/apps/client/src/widgets/view_widgets/view_mode.ts +++ b/apps/client/src/widgets/view_widgets/view_mode.ts @@ -7,7 +7,7 @@ import ViewModeStorage from "./view_mode_storage.js"; export interface ViewModeArgs { $parent: JQuery; parentNote: FNote; - parentNotePath: string | null | undefined; + parentNotePath?: string | null; noteIds: string[]; showNotePath?: boolean; } From 7c943fe4acebabfcdcf6c9ebab3ccedfd7f7b8c6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 1 Jul 2025 12:10:01 +0300 Subject: [PATCH 069/107] chore(view/table): leftover files --- .../table_view/header-add-column-button.ts | 46 ----------------- .../table_view/header-customization.ts | 49 ------------------- 2 files changed, 95 deletions(-) delete mode 100644 apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts delete mode 100644 apps/client/src/widgets/view_widgets/table_view/header-customization.ts diff --git a/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts b/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts deleted file mode 100644 index cf02950c3..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/header-add-column-button.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - IHeaderParams, - IHeaderComp, -} from 'ag-grid-community'; - -export default class TableAddColumnButton implements IHeaderComp { - private eGui!: HTMLElement; - private params!: IHeaderParams; - - public init(params: IHeaderParams): void { - this.params = params; - - const container = document.createElement('div'); - container.style.display = 'flex'; - container.style.justifyContent = 'space-between'; - container.style.alignItems = 'center'; - - const label = document.createElement('span'); - label.innerText = params.displayName; - - const button = document.createElement('button'); - button.textContent = '+'; - button.title = 'Add Row'; - button.onclick = () => { - alert(`Add row for column: ${params.displayName}`); - // Optionally trigger insert logic here - }; - - container.appendChild(label); - container.appendChild(button); - - this.eGui = container; - } - - public getGui(): HTMLElement { - return this.eGui; - } - - refresh(params: IHeaderParams): boolean { - return false; - } - - public destroy(): void { - // Optional: clean up if needed - } -} diff --git a/apps/client/src/widgets/view_widgets/table_view/header-customization.ts b/apps/client/src/widgets/view_widgets/table_view/header-customization.ts deleted file mode 100644 index 46484895f..000000000 --- a/apps/client/src/widgets/view_widgets/table_view/header-customization.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { GridApi } from "ag-grid-community"; -import contextMenu, { MenuItem } from "../../../menus/context_menu.js"; -import { TableData } from "./data.js"; - -export default function applyHeaderCustomization(baseEl: HTMLElement, api: GridApi) { - const header = baseEl.querySelector(".ag-header"); - if (!header) { - return; - } - - header.addEventListener("contextmenu", (_e) => { - const e = _e as MouseEvent; - e.preventDefault(); - - contextMenu.show({ - items: [ - { - title: "Columns", - items: buildColumnChooser(api) - } - ], - x: e.pageX, - y: e.pageY, - selectMenuItemHandler: () => {} - }); - }); -} - -export function buildColumnChooser(api: GridApi) { - const items: MenuItem[] = []; - - for (const column of api.getColumns() ?? []) { - const colDef = column.getColDef(); - if (!colDef) { - continue; - } - - const visible = column.isVisible(); - items.push({ - title: colDef.headerName ?? api.getDisplayNameForColumn(column, "header") ?? "", - checked: visible, - handler() { - api.setColumnsVisible([ column ], !visible); - } - }); - } - - return items; -} From f5dc4de1c123e929a7c893ae9ef10c30fafd9f28 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 14:12:36 +0300 Subject: [PATCH 070/107] feat(views/table): parse relations --- .../widgets/view_widgets/table_view/data.ts | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 3ad275821..1cc8b19e7 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -15,7 +15,7 @@ export type TableData = { export interface PromotedAttributeInformation { name: string; title?: string; - type?: LabelType; + type?: LabelType | "relation"; } const labelTypeMappings: Record> = { @@ -130,7 +130,9 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf const labels: typeof definitions[0]["labels"] = {}; for (const { name, type } of infos) { - if (type === "boolean") { + if (type === "relation") { + labels[name] = note.getRelationValue(name); + } else if (type === "boolean") { labels[name] = note.hasLabel(name); } else { labels[name] = note.getLabelValue(name); @@ -146,28 +148,37 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf }); } + console.log("Built row definitions", definitions); + return definitions; } export default function getPromotedAttributeInformation(parentNote: FNote) { const info: PromotedAttributeInformation[] = []; for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) { - if (promotedAttribute.type !== "label") { - console.warn("Relations are not supported for now"); - continue; - } - const def = promotedAttribute.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") { + console.warn("Relations are not supported for now"); + continue; + } + + let type: LabelType | "relation" = def.labelType || "text"; + if (labelType === "relation") { + type = "relation"; + } + info.push({ - name: promotedAttribute.name.split(":", 2)[1], + name, title: def.promotedAlias, - type: def.labelType - }) + type + }); } + console.log("Promoted attribute information", info); return info; } From 6456bb34ae086b361a89afcf976e8a083a12752d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 14:29:40 +0300 Subject: [PATCH 071/107] chore(views/table): start implementing a relation editor --- .../table_view/relation_editor.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 apps/client/src/widgets/view_widgets/table_view/relation_editor.ts diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts new file mode 100644 index 000000000..4f4f17f52 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -0,0 +1,39 @@ +import { CellComponent } from "tabulator-tables"; + +export default function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ + //cell - the cell component for the editable cell + //onRendered - function to call when the editor has been rendered + //success - function to call to pass thesuccessfully updated value to Tabulator + //cancel - function to call to abort the edit and return to a normal cell + //editorParams - params object passed into the editorParams column definition property + + //create and style editor + var editor = document.createElement("input"); + + editor.setAttribute("type", "date"); + + //create and style input + editor.style.padding = "3px"; + editor.style.width = "100%"; + editor.style.boxSizing = "border-box"; + + //Set value of editor to the current value of the cell + editor.value = cell.getValue(); + + //set focus on the select box when the editor is selected + onRendered(function(){ + editor.focus(); + editor.style.css = "100%"; + }); + + //when the value has been set, trigger the cell to update + function successFunc(){ + success("Hi"); + } + + editor.addEventListener("change", successFunc); + editor.addEventListener("blur", successFunc); + + //return the editor element + return editor; +}; From 8614d39ef439cff768d076f632833f5fcc93c11b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 14:42:04 +0300 Subject: [PATCH 072/107] chore(views/table): remove unnecessary log --- apps/client/src/widgets/view_widgets/table_view/data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 1cc8b19e7..fd667ca0a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -110,7 +110,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { formatter: () => ``, cellClick: (e, cell) => { const noteId = cell.getRow().getCell("noteId").getValue(); - console.log("Got note ID", noteId); if (noteId) { link.goToLinkExt(e as MouseEvent, `#root/${noteId}`); } From a2e197facd4bfed84fdde1dff4a94a88868c385f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 14:50:07 +0300 Subject: [PATCH 073/107] feat(views/table): set up relation editor --- .../src/widgets/view_widgets/table_view/data.ts | 10 ++++++++-- .../view_widgets/table_view/relation_editor.ts | 11 ++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index fd667ca0a..a86886f70 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -2,6 +2,7 @@ import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; import type { ColumnDefinition } from "tabulator-tables"; import link from "../../../services/link.js"; +import RelationEditor from "./relation_editor.js"; export type TableData = { iconClass: string; @@ -12,13 +13,15 @@ export type TableData = { position: number; }; +type ColumnType = LabelType | "relation"; + export interface PromotedAttributeInformation { name: string; title?: string; - type?: LabelType | "relation"; + type?: ColumnType; } -const labelTypeMappings: Record> = { +const labelTypeMappings: Record> = { text: { editor: "input" }, @@ -41,6 +44,9 @@ const labelTypeMappings: Record> = { url: { formatter: "link", editor: "input" + }, + relation: { + editor: RelationEditor } }; diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 4f4f17f52..34efad4f6 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -1,4 +1,5 @@ import { CellComponent } from "tabulator-tables"; +import note_autocomplete from "../../../services/note_autocomplete"; export default function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ //cell - the cell component for the editable cell @@ -8,9 +9,9 @@ export default function RelationEditor(cell: CellComponent, onRendered, success, //editorParams - params object passed into the editorParams column definition property //create and style editor - var editor = document.createElement("input"); - - editor.setAttribute("type", "date"); + const editor = document.createElement("input"); + const $editor = $(editor); + editor.classList.add("form-control"); //create and style input editor.style.padding = "3px"; @@ -22,13 +23,13 @@ export default function RelationEditor(cell: CellComponent, onRendered, success, //set focus on the select box when the editor is selected onRendered(function(){ + note_autocomplete.initNoteAutocomplete($editor); editor.focus(); - editor.style.css = "100%"; }); //when the value has been set, trigger the cell to update function successFunc(){ - success("Hi"); + success($editor.getSelectedNoteId()); } editor.addEventListener("change", successFunc); From b29364339891ab0bdabcca58a645fb763d9e7fcb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 15:02:10 +0300 Subject: [PATCH 074/107] feat(views/table): basic formatter for relations --- .../src/widgets/view_widgets/table_view/data.ts | 5 +++-- .../view_widgets/table_view/relation_editor.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index a86886f70..4dd8c7b12 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -2,7 +2,7 @@ import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; import type { ColumnDefinition } from "tabulator-tables"; import link from "../../../services/link.js"; -import RelationEditor from "./relation_editor.js"; +import { RelationEditor, RelationFormatter } from "./relation_editor.js"; export type TableData = { iconClass: string; @@ -46,7 +46,8 @@ const labelTypeMappings: Record> = { editor: "input" }, relation: { - editor: RelationEditor + editor: RelationEditor, + formatter: RelationFormatter } }; diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 34efad4f6..5b463afd1 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -1,7 +1,7 @@ import { CellComponent } from "tabulator-tables"; import note_autocomplete from "../../../services/note_autocomplete"; -export default function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ +export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ //cell - the cell component for the editable cell //onRendered - function to call when the editor has been rendered //success - function to call to pass thesuccessfully updated value to Tabulator @@ -38,3 +38,12 @@ export default function RelationEditor(cell: CellComponent, onRendered, success, //return the editor element return editor; }; + +export function RelationFormatter(cell: CellComponent, formatterParams, onRendered) { + const noteId = cell.getValue(); + if (!noteId) { + return ""; + } + + return `Title goes here`; +} From a4664576fe80d35e89e3dd10be7276be00f7d2d5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 15:05:00 +0300 Subject: [PATCH 075/107] feat(views/table): separate data model for relations --- apps/client/src/widgets/view_widgets/table_view/data.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 4dd8c7b12..37240e71c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -9,6 +9,7 @@ export type TableData = { noteId: string; title: string; labels: Record; + relations: Record; branchId: string; position: number; }; @@ -100,8 +101,10 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { ]; for (const { name, title, type } of info) { + const prefix = (type === "relation" ? "relations" : "labels"); + columnDefs.push({ - field: `labels.${name}`, + field: `${prefix}.${name}`, title: title ?? name, editor: "input", ...labelTypeMappings[type ?? "text"], @@ -135,9 +138,10 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf } const labels: typeof definitions[0]["labels"] = {}; + const relations: typeof definitions[0]["relations"] = {}; for (const { name, type } of infos) { if (type === "relation") { - labels[name] = note.getRelationValue(name); + relations[name] = note.getRelationValue(name); } else if (type === "boolean") { labels[name] = note.hasLabel(name); } else { @@ -149,6 +153,7 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf noteId: note.noteId, title: note.title, labels, + relations, position: branch.notePosition, branchId: branch.branchId }); From 45ac70b78ff22be911c388c005e7ce5bc40ee5db Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 15:07:40 +0300 Subject: [PATCH 076/107] feat(views/table): proper storage of relations --- apps/client/src/services/attributes.ts | 2 +- .../widgets/view_widgets/table_view/index.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 95fb75726..52ea8967a 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -50,7 +50,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) { * @param name the name of the attribute to set. * @param value the value of the attribute to set. */ -async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { +export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { if (value) { // Create or update the attribute. await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); 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 5a06384cb..41f6fa39a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -1,6 +1,6 @@ import froca from "../../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "../view_mode.js"; -import attributes, { setLabel } from "../../../services/attributes.js"; +import attributes, { setAttribute, setLabel } from "../../../services/attributes.js"; import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js"; import server from "../../../services/server.js"; import SpacedUpdate from "../../../services/spaced_update.js"; @@ -120,7 +120,7 @@ export default class TableView extends ViewMode { } private setupEditing() { - this.api!.on("cellEdited", (cell) => { + this.api!.on("cellEdited", async (cell) => { const noteId = cell.getRow().getData().noteId; const field = cell.getField(); const newValue = cell.getValue(); @@ -130,9 +130,16 @@ export default class TableView extends ViewMode { return; } - if (field.startsWith("labels.")) { - const labelName = field.split(".", 2)[1]; - setLabel(noteId, labelName, newValue); + if (field.includes(".")) { + const [ type, name ] = field.split(".", 2); + if (type === "labels") { + setLabel(noteId, name, newValue); + } else if (type === "relations") { + const note = await froca.getNote(noteId); + if (note) { + setAttribute(note, "relation", name, newValue); + } + } } }); } From ac70908c5ae0cff42746fd9f8197dacc2c2cfe05 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 16:14:14 +0300 Subject: [PATCH 077/107] feat(views/table): integrate reference-like for relations --- apps/client/src/services/link.ts | 2 +- .../widgets/view_widgets/table_view/relation_editor.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index 116ca8a5b..41533647c 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -384,7 +384,7 @@ function linkContextMenu(e: PointerEvent) { linkContextMenuService.openContextMenu(notePath, e, viewScope, null); } -async function loadReferenceLinkTitle($el: JQuery, href: string | null | undefined = null) { +export async function loadReferenceLinkTitle($el: JQuery, href: string | null | undefined = null) { const $link = $el[0].tagName === "A" ? $el : $el.find("a"); href = href || $link.attr("href"); diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 5b463afd1..fdc483b26 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -1,5 +1,6 @@ import { CellComponent } from "tabulator-tables"; import note_autocomplete from "../../../services/note_autocomplete"; +import { loadReferenceLinkTitle } from "../../../services/link"; export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ //cell - the cell component for the editable cell @@ -45,5 +46,12 @@ export function RelationFormatter(cell: CellComponent, formatterParams, onRender return ""; } - return `Title goes here`; + onRendered(async () => { + const $link = $(""); + $link.addClass("reference-link"); + $link.attr("href", `#root/${noteId}`); + await loadReferenceLinkTitle($link); + cell.getElement().appendChild($link[0]); + }); + return ""; } From 4ac7b6e9e868e7a97ddeb4bbb91b162365352ea2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 17:17:39 +0300 Subject: [PATCH 078/107] feat(views/table): allow creation of new notes --- .../table_view/relation_editor.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index fdc483b26..c6049026a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -24,18 +24,20 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, //set focus on the select box when the editor is selected onRendered(function(){ - note_autocomplete.initNoteAutocomplete($editor); + note_autocomplete.initNoteAutocomplete($editor, { + allowCreatingNotes: true + }).on("autocomplete:noteselected", (event, suggestion, dataset) => { + const notePath = suggestion.notePath; + if (!notePath) { + return; + } + + const noteId = notePath.split("/").at(-1); + success(noteId); + }); editor.focus(); }); - //when the value has been set, trigger the cell to update - function successFunc(){ - success($editor.getSelectedNoteId()); - } - - editor.addEventListener("change", successFunc); - editor.addEventListener("blur", successFunc); - //return the editor element return editor; }; From 854969e1b85152470d35a5d24342adf16dfcb169 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 18:05:24 +0300 Subject: [PATCH 079/107] feat(views/table): react to external attribute changes --- apps/client/src/widgets/view_widgets/table_view/index.ts | 4 ++++ 1 file changed, 4 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 41f6fa39a..bd65176d1 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -227,6 +227,10 @@ export default class TableView extends ViewMode { this.#manageRowsUpdate(); } + if (loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!))) { + this.#manageRowsUpdate(); + } + return false; } From e411f9932f48a4b73695145b7b8ecd1ec97a1b77 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 18:13:07 +0300 Subject: [PATCH 080/107] feat(views/table): display note title when editing relation --- apps/client/src/widgets/view_widgets/table_view/data.ts | 2 -- .../src/widgets/view_widgets/table_view/relation_editor.ts | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 37240e71c..2afd7b851 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -159,8 +159,6 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf }); } - console.log("Built row definitions", definitions); - return definitions; } diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index c6049026a..9db63af14 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -1,6 +1,7 @@ import { CellComponent } from "tabulator-tables"; import note_autocomplete from "../../../services/note_autocomplete"; import { loadReferenceLinkTitle } from "../../../services/link"; +import froca from "../../../services/froca"; export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ //cell - the cell component for the editable cell @@ -20,7 +21,11 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editor.style.boxSizing = "border-box"; //Set value of editor to the current value of the cell - editor.value = cell.getValue(); + const noteId = cell.getValue(); + if (noteId) { + const note = froca.getNoteFromCache(noteId); + editor.value = note.title; + } //set focus on the select box when the editor is selected onRendered(function(){ From dcad23316d378383ae472472733a6c155a2e8225 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 18:26:24 +0300 Subject: [PATCH 081/107] style(views/table): improve autocomplete styling --- apps/client/src/widgets/view_widgets/table_view/index.ts | 8 ++++++++ .../widgets/view_widgets/table_view/relation_editor.ts | 8 ++++++-- 2 files changed, 14 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 bd65176d1..a16cbb9c5 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -34,6 +34,14 @@ const TPL = /*html*/` right: 0; bottom: 0; } + + .tabulator-cell .autocomplete { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: transparent; + outline: none !important; + }
diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 9db63af14..6a1bc334b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -12,6 +12,7 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, //create and style editor const editor = document.createElement("input"); + const $editor = $(editor); editor.classList.add("form-control"); @@ -43,8 +44,11 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editor.focus(); }); - //return the editor element - return editor; + const container = document.createElement("div"); + container.classList.add("input-group"); + container.classList.add("autocomplete"); + container.appendChild(editor); + return container; }; export function RelationFormatter(cell: CellComponent, formatterParams, onRendered) { From c69ef611a08107811f5c4a791eb7b09d11cf95e6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 18:53:31 +0300 Subject: [PATCH 082/107] feat(views/table): basic reordering mechanism --- .../view_widgets/table_view/dragging.ts | 24 +++++++++++ .../widgets/view_widgets/table_view/index.ts | 41 ++++--------------- 2 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/dragging.ts diff --git a/apps/client/src/widgets/view_widgets/table_view/dragging.ts b/apps/client/src/widgets/view_widgets/table_view/dragging.ts new file mode 100644 index 000000000..598e5a34a --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/dragging.ts @@ -0,0 +1,24 @@ +import type { Tabulator } from "tabulator-tables"; +import type FNote from "../../../entities/fnote.js"; +import branches from "../../../services/branches.js"; + +export function canReorderRows(parentNote: FNote) { + return !parentNote.hasLabel("sorted"); +} + +export function configureReorderingRows(tabulator: Tabulator) { + tabulator.on("rowMoved", (row) => { + const branchIdsToMove = [ row.getData().branchId ]; + + const prevRow = row.getPrevRow(); + if (prevRow) { + branches.moveAfterBranch(branchIdsToMove, prevRow.getData().branchId); + return; + } + + const nextRow = row.getNextRow(); + if (nextRow) { + branches.moveBeforeBranch(branchIdsToMove, nextRow.getData().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 a16cbb9c5..a3b6d531b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -8,9 +8,10 @@ import branches from "../../../services/branches.js"; import type { CommandListenerData, EventData } from "../../../components/app_context.js"; import type { Attribute } from "../../../services/attribute_parser.js"; import note_create from "../../../services/note_create.js"; -import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule} from 'tabulator-tables'; +import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule, MoveRowsModule} from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; import { applyHeaderMenu } from "./header-menu.js"; +import { canReorderRows, configureReorderingRows } from "./dragging.js"; const TPL = /*html*/`
@@ -89,7 +90,7 @@ export default class TableView extends ViewMode { } private async renderTable(el: HTMLElement) { - const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule]; + const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, MenuModule]; for (const module of modules) { Tabulator.registerModule(module); } @@ -106,18 +107,23 @@ export default class TableView extends ViewMode { const viewStorage = await this.viewStorage.restore(); this.persistentData = viewStorage?.tableData || {}; + + const movableRows = canReorderRows(this.parentNote); + this.api = new Tabulator(el, { index: "noteId", columns: columnDefs, data: await buildRowDefinitions(this.parentNote, notes, info), persistence: true, movableColumns: true, + movableRows, persistenceWriterFunc: (_id, type: string, data: object) => { this.persistentData[type] = data; this.spacedUpdate.scheduleUpdate(); }, persistenceReaderFunc: (_id, type: string) => this.persistentData[type], }); + configureReorderingRows(this.api); this.setupEditing(); } @@ -152,37 +158,6 @@ export default class TableView extends ViewMode { }); } - private setupDragging() { - if (this.parentNote.hasLabel("sorted")) { - return {}; - } - - const config: GridOptions = { - rowDragEntireRow: true, - onRowDragEnd(e) { - const fromIndex = e.node.rowIndex; - const toIndex = e.overNode?.rowIndex; - if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) { - return; - } - - const isBelow = (toIndex > fromIndex); - const fromBranchId = e.node.data?.branchId; - const toBranchId = e.overNode?.data?.branchId; - if (fromBranchId === undefined || toBranchId === undefined) { - return; - } - - if (isBelow) { - branches.moveAfterBranch([ fromBranchId ], toBranchId); - } else { - branches.moveBeforeBranch([ fromBranchId ], toBranchId); - } - } - }; - return config; - } - async reloadAttributesCommand() { console.log("Reload attributes"); } From 01b2257063a74b46602dddb75fd69c0848418b8a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:23:26 +0300 Subject: [PATCH 083/107] feat(views/table): relocate new row/column buttons --- apps/client/src/translations/en/translation.json | 4 ++++ .../widgets/view_widgets/table_view/footer.ts | 13 +++++++++++++ .../src/widgets/view_widgets/table_view/index.ts | 16 +++++++++++----- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/footer.ts diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index fadf09a4a..b2d2f0a27 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1934,5 +1934,9 @@ "title": "Features", "emoji_completion_enabled": "Enable Emoji auto-completion", "note_completion_enabled": "Enable note auto-completion" + }, + "table_view": { + "new-row": "New row", + "new-column": "New column" } } diff --git a/apps/client/src/widgets/view_widgets/table_view/footer.ts b/apps/client/src/widgets/view_widgets/table_view/footer.ts new file mode 100644 index 000000000..2d168b8cd --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/footer.ts @@ -0,0 +1,13 @@ +import { t } from "../../../services/i18n.js"; + +export default function buildFooter() { + return /*html*/`\ + + + + `.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 a3b6d531b..b26c2bc0b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -12,6 +12,7 @@ import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, Resi import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css"; import { applyHeaderMenu } from "./header-menu.js"; import { canReorderRows, configureReorderingRows } from "./dragging.js"; +import buildFooter from "./footer.js"; const TPL = /*html*/`
@@ -43,12 +44,16 @@ const TPL = /*html*/` background: transparent; outline: none !important; } - -
- - -
+ .tabulator .tabulator-footer { + background-color: unset; + } + + .tabulator .tabulator-footer .tabulator-footer-contents { + justify-content: left; + gap: 0.5em; + } +
@@ -117,6 +122,7 @@ export default class TableView extends ViewMode { persistence: true, movableColumns: true, movableRows, + footerElement: buildFooter(), persistenceWriterFunc: (_id, type: string, data: object) => { this.persistentData[type] = data; this.spacedUpdate.scheduleUpdate(); From 323e3d3cac84db483b04707b8ae46c6775e9ad96 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:25:08 +0300 Subject: [PATCH 084/107] feat(views/table): hide note ID by default --- apps/client/src/widgets/view_widgets/table_view/data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 2afd7b851..0ed508622 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -88,6 +88,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { { field: "noteId", title: "Note ID", + visible: false }, { field: "title", From d5327b3b4ad795b09ea6b0181dfdd9d44a8369f5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:26:06 +0300 Subject: [PATCH 085/107] feat(views/table): get rid of note position column --- apps/client/src/widgets/view_widgets/table_view/data.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 0ed508622..9866fbb49 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -11,7 +11,6 @@ export type TableData = { labels: Record; relations: Record; branchId: string; - position: number; }; type ColumnType = LabelType | "relation"; @@ -94,10 +93,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { field: "title", title: "Title", editor: "input" - }, - { - field: "position", - title: "Position" } ]; @@ -155,7 +150,6 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf title: note.title, labels, relations, - position: branch.notePosition, branchId: branch.branchId }); } From ae9b2c08a9d76c4a7acf97820b9206cbc4f353c7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:29:33 +0300 Subject: [PATCH 086/107] feat(views/table): hide context menu for small columns --- .../src/widgets/view_widgets/table_view/header-menu.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index 4f53fbbe4..a514f8d26 100644 --- a/apps/client/src/widgets/view_widgets/table_view/header-menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/header-menu.ts @@ -1,9 +1,10 @@ -import type { CellComponent, MenuObject, Tabulator } from "tabulator-tables"; +import type { CellComponent, ColumnComponent, MenuObject, Tabulator } from "tabulator-tables"; -export function applyHeaderMenu(columns) { - //apply header menu to each column +export function applyHeaderMenu(columns: ColumnComponent[]) { for (let column of columns) { - column.headerMenu = headerMenu; + if (column.headerSort !== false) { + column.headerMenu = headerMenu; + } } } From 513636e1e0461628056edc858064b56f53e97e3e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:32:22 +0300 Subject: [PATCH 087/107] feat(views/table): hide column titles for small ones --- apps/client/src/widgets/view_widgets/table_view/data.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index 9866fbb49..dba476fb1 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -64,6 +64,7 @@ export async function buildData(parentNote: FNote, info: PromotedAttributeInform } export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { + const emptyTitleFormatter = () => ""; const columnDefs: ColumnDefinition[] = [ { title: "#", @@ -75,7 +76,8 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }, { field: "iconClass", - title: "Icon", + title: "Note icon", + titleFormatter: emptyTitleFormatter, width: 40, headerSort: false, hozAlign: "center", @@ -109,11 +111,12 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { // End actions columnDefs.push({ - title: "Open note", + title: "Open note button", width: 40, hozAlign: "center", headerSort: false, formatter: () => ``, + titleFormatter: emptyTitleFormatter, cellClick: (e, cell) => { const noteId = cell.getRow().getCell("noteId").getValue(); if (noteId) { From 5f8ef0395b825d11cd6cea0ad33a9461322943d2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:37:05 +0300 Subject: [PATCH 088/107] feat(views/table): improve default layout --- apps/client/src/widgets/view_widgets/table_view/data.ts | 3 ++- apps/client/src/widgets/view_widgets/table_view/index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index dba476fb1..e562f80eb 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -94,7 +94,8 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { { field: "title", title: "Title", - editor: "input" + editor: "input", + width: 400 } ]; 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 b26c2bc0b..12cd8d742 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -116,6 +116,7 @@ export default class TableView extends ViewMode { const movableRows = canReorderRows(this.parentNote); this.api = new Tabulator(el, { + layout: "fitDataFill", index: "noteId", columns: columnDefs, data: await buildRowDefinitions(this.parentNote, notes, info), From 15c593f68ee9d4aae97418e70a187c334d8386d3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:46:37 +0300 Subject: [PATCH 089/107] feat(views/table): automatically focus on title when creating new row --- .../widgets/view_widgets/table_view/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 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 12cd8d742..f130b440c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -72,6 +72,8 @@ export default class TableView extends ViewMode { private api?: Tabulator; private newAttribute?: Attribute; private persistentData: Record; + /** 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; constructor(args: ViewModeArgs) { super(args, "table"); @@ -188,7 +190,12 @@ export default class TableView extends ViewMode { if (parentNotePath) { note_create.createNote(parentNotePath, { activate: false - }); + }).then(({ note }) => { + if (!note) { + return; + } + this.noteIdToEdit = note.noteId; + }) } } @@ -242,6 +249,14 @@ export default class TableView extends ViewMode { const notes = await froca.getNotes(this.args.noteIds); const info = getPromotedAttributeInformation(this.parentNote); this.api.setData(await buildRowDefinitions(this.parentNote, notes, info)); + + if (this.noteIdToEdit) { + const row = this.api?.getRows(true).find(r => r.getData().noteId === this.noteIdToEdit); + if (row) { + row.getCell("title").edit(); + } + this.noteIdToEdit = undefined; + } } } From 7f5a1ee45af73f92a8e49300a3b3b26c8b76e664 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 19:47:52 +0300 Subject: [PATCH 090/107] feat(ribbon): stop focusing book tab by default --- apps/client/src/widgets/ribbon_widgets/book_properties.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/client/src/widgets/ribbon_widgets/book_properties.ts b/apps/client/src/widgets/ribbon_widgets/book_properties.ts index 5be7a86c5..cd9735b20 100644 --- a/apps/client/src/widgets/ribbon_widgets/book_properties.ts +++ b/apps/client/src/widgets/ribbon_widgets/book_properties.ts @@ -68,7 +68,6 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { getTitle() { return { show: this.isEnabled(), - activate: true, title: t("book_properties.book_properties"), icon: "bx bx-book" }; From e5b10ab16ab2d56af8d01535ccfbdc6b526402b4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:08:41 +0300 Subject: [PATCH 091/107] feat(views/table): set up relations not as a link --- apps/client/src/services/note_tooltip.ts | 1 + .../view_widgets/table_view/relation_editor.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/client/src/services/note_tooltip.ts b/apps/client/src/services/note_tooltip.ts index 4a2b88bb6..d83ad3afa 100644 --- a/apps/client/src/services/note_tooltip.ts +++ b/apps/client/src/services/note_tooltip.ts @@ -14,6 +14,7 @@ let dismissTimer: ReturnType; function setupGlobalTooltip() { $(document).on("mouseenter", "a", mouseEnterHandler); + $(document).on("mouseenter", "[data-href]", mouseEnterHandler); // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen $(document).on("click", (e) => { diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 6a1bc334b..37026055c 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -58,11 +58,13 @@ export function RelationFormatter(cell: CellComponent, formatterParams, onRender } onRendered(async () => { - const $link = $("
"); - $link.addClass("reference-link"); - $link.attr("href", `#root/${noteId}`); - await loadReferenceLinkTitle($link); - cell.getElement().appendChild($link[0]); + const $noteRef = $(""); + const href = `#root/${noteId}`; + $noteRef.addClass("reference-link"); + $noteRef.attr("data-href", href); + + await loadReferenceLinkTitle($noteRef, href); + cell.getElement().appendChild($noteRef[0]); }); return ""; } From 08cf95aa389a8562eda9a9a81f2b9fda18ca2d9f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:22:55 +0300 Subject: [PATCH 092/107] feat(views/table): merge open note and icon into title --- .../widgets/view_widgets/table_view/data.ts | 35 ++-------------- .../view_widgets/table_view/formatters.ts | 41 +++++++++++++++++++ .../table_view/relation_editor.ts | 19 --------- 3 files changed, 45 insertions(+), 50 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/table_view/formatters.ts diff --git a/apps/client/src/widgets/view_widgets/table_view/data.ts b/apps/client/src/widgets/view_widgets/table_view/data.ts index e562f80eb..b84b4d50f 100644 --- a/apps/client/src/widgets/view_widgets/table_view/data.ts +++ b/apps/client/src/widgets/view_widgets/table_view/data.ts @@ -1,8 +1,8 @@ import FNote from "../../../entities/fnote.js"; import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js"; import type { ColumnDefinition } from "tabulator-tables"; -import link from "../../../services/link.js"; -import { RelationEditor, RelationFormatter } from "./relation_editor.js"; +import { RelationEditor } from "./relation_editor.js"; +import { NoteFormatter, NoteTitleFormatter } from "./formatters.js"; export type TableData = { iconClass: string; @@ -47,7 +47,7 @@ const labelTypeMappings: Record> = { }, relation: { editor: RelationEditor, - formatter: RelationFormatter + formatter: NoteFormatter } }; @@ -74,18 +74,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { resizable: false, frozen: true }, - { - field: "iconClass", - title: "Note icon", - titleFormatter: emptyTitleFormatter, - width: 40, - headerSort: false, - hozAlign: "center", - formatter(cell) { - const iconClass = cell.getValue(); - return ``; - }, - }, { field: "noteId", title: "Note ID", @@ -95,6 +83,7 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { field: "title", title: "Title", editor: "input", + formatter: NoteTitleFormatter, width: 400 } ]; @@ -110,22 +99,6 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[]) { }); } - // End actions - columnDefs.push({ - title: "Open note button", - width: 40, - hozAlign: "center", - headerSort: false, - formatter: () => ``, - titleFormatter: emptyTitleFormatter, - cellClick: (e, cell) => { - const noteId = cell.getRow().getCell("noteId").getValue(); - if (noteId) { - link.goToLinkExt(e as MouseEvent, `#root/${noteId}`); - } - } - }); - return columnDefs; } diff --git a/apps/client/src/widgets/view_widgets/table_view/formatters.ts b/apps/client/src/widgets/view_widgets/table_view/formatters.ts new file mode 100644 index 000000000..8276bcdc4 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/table_view/formatters.ts @@ -0,0 +1,41 @@ +import { CellComponent } from "tabulator-tables"; +import { loadReferenceLinkTitle } from "../../../services/link.js"; + +/** + * Custom formatter to represent a note, with the icon and note title being rendered. + * + * The value of the cell must be the note ID. + */ +export function NoteFormatter(cell: CellComponent, formatterParams, onRendered) { + let noteId = cell.getValue(); + if (!noteId) { + return ""; + } + + onRendered(async () => { + const $noteRef = $(""); + const href = `#root/${noteId}`; + $noteRef.addClass("reference-link"); + $noteRef.attr("data-href", href); + + await loadReferenceLinkTitle($noteRef, href); + cell.getElement().appendChild($noteRef[0]); + }); + return ""; +} + +export function NoteTitleFormatter(cell: CellComponent, formatterParams, onRendered) { + const { noteId, iconClass } = cell.getRow().getData(); + if (!noteId) { + return ""; + } + + const $noteRef = $(""); + const href = `#root/${noteId}`; + $noteRef.addClass("reference-link"); + $noteRef.attr("data-href", href); + $noteRef.text(cell.getValue()); + $noteRef.prepend($("").addClass(iconClass)); + + return $noteRef[0].outerHTML; +} diff --git a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts index 37026055c..0bd1cb1f2 100644 --- a/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts +++ b/apps/client/src/widgets/view_widgets/table_view/relation_editor.ts @@ -1,6 +1,5 @@ import { CellComponent } from "tabulator-tables"; import note_autocomplete from "../../../services/note_autocomplete"; -import { loadReferenceLinkTitle } from "../../../services/link"; import froca from "../../../services/froca"; export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){ @@ -50,21 +49,3 @@ export function RelationEditor(cell: CellComponent, onRendered, success, cancel, container.appendChild(editor); return container; }; - -export function RelationFormatter(cell: CellComponent, formatterParams, onRendered) { - const noteId = cell.getValue(); - if (!noteId) { - return ""; - } - - onRendered(async () => { - const $noteRef = $(""); - const href = `#root/${noteId}`; - $noteRef.addClass("reference-link"); - $noteRef.attr("data-href", href); - - await loadReferenceLinkTitle($noteRef, href); - cell.getElement().appendChild($noteRef[0]); - }); - return ""; -} From 60963abe2c7709262ca658b30b2d3a42db240687 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:32:53 +0300 Subject: [PATCH 093/107] refactor(views/table): reduce duplication --- .../view_widgets/table_view/formatters.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) 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 8276bcdc4..68e529798 100644 --- a/apps/client/src/widgets/view_widgets/table_view/formatters.ts +++ b/apps/client/src/widgets/view_widgets/table_view/formatters.ts @@ -6,36 +6,40 @@ import { loadReferenceLinkTitle } from "../../../services/link.js"; * * The value of the cell must be the note ID. */ -export function NoteFormatter(cell: CellComponent, formatterParams, onRendered) { +export function NoteFormatter(cell: CellComponent, _formatterParams, onRendered) { let noteId = cell.getValue(); if (!noteId) { return ""; } onRendered(async () => { - const $noteRef = $(""); - const href = `#root/${noteId}`; - $noteRef.addClass("reference-link"); - $noteRef.attr("data-href", href); - + const { $noteRef, href } = buildNoteLink(noteId); await loadReferenceLinkTitle($noteRef, href); cell.getElement().appendChild($noteRef[0]); }); return ""; } -export function NoteTitleFormatter(cell: CellComponent, formatterParams, onRendered) { +/** + * Custom formatter for the note title that is quite similar to {@link NoteFormatter}, but where the title and icons are read from separate fields. + */ +export function NoteTitleFormatter(cell: CellComponent) { const { noteId, iconClass } = cell.getRow().getData(); if (!noteId) { return ""; } - const $noteRef = $(""); - const href = `#root/${noteId}`; - $noteRef.addClass("reference-link"); - $noteRef.attr("data-href", href); + const { $noteRef } = buildNoteLink(noteId); $noteRef.text(cell.getValue()); $noteRef.prepend($("").addClass(iconClass)); return $noteRef[0].outerHTML; } + +function buildNoteLink(noteId: string) { + const $noteRef = $(""); + const href = `#root/${noteId}`; + $noteRef.addClass("reference-link"); + $noteRef.attr("data-href", href); + return { $noteRef, href }; +} From 0f7a2adf1580248f1202d8610a72b8e481767c58 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:38:48 +0300 Subject: [PATCH 094/107] feat(views/table): improve layout --- .../src/widgets/view_widgets/table_view/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 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 f130b440c..86d151f4d 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -22,7 +22,6 @@ const TPL = /*html*/` position: relative; height: 100%; user-select: none; - padding: 10px; } .table-view-container { @@ -45,8 +44,19 @@ const TPL = /*html*/` outline: none !important; } + .tabulator .tabulator-header { + border-top: unset; + border-bottom-width: 1px; + } + + .tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left, + .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left { + border-right-width: 1px; + } + .tabulator .tabulator-footer { background-color: unset; + padding: 5px 0; } .tabulator .tabulator-footer .tabulator-footer-contents { From 63537aff20bbf778ec10f4610cf5e451512b89c4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:43:16 +0300 Subject: [PATCH 095/107] feat(views/table): disable reordering in search --- apps/client/src/widgets/view_widgets/table_view/dragging.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/dragging.ts b/apps/client/src/widgets/view_widgets/table_view/dragging.ts index 598e5a34a..39d5a0178 100644 --- a/apps/client/src/widgets/view_widgets/table_view/dragging.ts +++ b/apps/client/src/widgets/view_widgets/table_view/dragging.ts @@ -3,7 +3,8 @@ import type FNote from "../../../entities/fnote.js"; import branches from "../../../services/branches.js"; export function canReorderRows(parentNote: FNote) { - return !parentNote.hasLabel("sorted"); + return !parentNote.hasLabel("sorted") + && parentNote.type !== "search"; } export function configureReorderingRows(tabulator: Tabulator) { From 4ded5e2b988f6f50f0e5b8366b5133e5ab39ee38 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 4 Jul 2025 20:56:10 +0300 Subject: [PATCH 096/107] feat(views/table): hide footer in search --- .../src/widgets/view_widgets/table_view/footer.ts | 11 ++++++++++- .../src/widgets/view_widgets/table_view/index.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) 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 2d168b8cd..fc533d6a7 100644 --- a/apps/client/src/widgets/view_widgets/table_view/footer.ts +++ b/apps/client/src/widgets/view_widgets/table_view/footer.ts @@ -1,6 +1,15 @@ +import FNote from "../../../entities/fnote.js"; import { t } from "../../../services/i18n.js"; -export default function buildFooter() { +function shouldDisplayFooter(parentNote: FNote) { + return (parentNote.type !== "search"); +} + +export default function buildFooter(parentNote: FNote) { + if (!shouldDisplayFooter(parentNote)) { + return undefined; + } + return /*html*/`\