From ef1153d3363e13e9c20d9670a16c790ce6fdd8cb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 18 Jul 2025 20:37:16 +0300 Subject: [PATCH 01/17] fix(views/table): insert direction no longer working --- .../src/widgets/view_widgets/table_view/context_menu.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index 57735cc44..efb28d5b9 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -253,7 +253,8 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, dir handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, - type: "label" + type: "label", + direction }); } }, @@ -262,7 +263,8 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, dir handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, - type: "relation" + type: "relation", + direction }); } } From ba5ef93c1acd9aa33a2426608182f05165cb087a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 18 Jul 2025 21:07:16 +0300 Subject: [PATCH 02/17] fix(views/table): wrong type when renaming relations --- .../src/widgets/view_widgets/table_view/col_editing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts index 48a39d031..ac8c7e740 100644 --- a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts +++ b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts @@ -78,14 +78,14 @@ export default class TableColumnEditing extends Component { return; } - const { name, type, value } = this.newAttribute; + const { name, value } = this.newAttribute; this.api.blockRedraw(); try { if (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name) { const oldName = this.existingAttributeToEdit.name.split(":")[1]; - const newName = name.split(":")[1]; - await renameColumn(this.parentNote.noteId, type, oldName, newName); + const [ type, newName ] = name.split(":"); + await renameColumn(this.parentNote.noteId, type as "label" | "relation", oldName, newName); } attributes.setLabel(this.parentNote.noteId, name, value); From 56553078ef03c7eb70ad0c6a1a85d1f78d4365df Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 09:47:10 +0300 Subject: [PATCH 03/17] docs(views/table): update documentation --- .../Notes/Note List/Table View.html | 191 +++++++++++++----- docs/User Guide/!!!meta.json | 21 +- .../Notes/Note List/Table View.md | 108 ++++++++-- 3 files changed, 245 insertions(+), 75 deletions(-) diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html index 7df7457b0..6a78bf2cc 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html @@ -5,79 +5,176 @@

The table view displays information in a grid, where the rows are individual notes and the columns are Promoted Attributes. In addition, values are editable.

+

How it works

+

The tabular structure is represented as such:

+
    +
  • Each child note is a row in the table.
  • +
  • If child rows also have children, they will be displayed under an expander + (nested notes).
  • +
  • Each column is a promoted attribute that + is defined on the Collection note. +
      +
    • Actually, both promoted and unpromoted attributes are supported, but it's + a requirement to use a label/relation definition.
    • +
    • The promoted attributes are usually defined as inheritable in order to + show up in the child notes, but it's not a requirement.
    • +
    +
  • +
  • If there are multiple attribute definitions with the same name, + only one will be displayed.
  • +
+

There are also a few predefined columns:

+
    +
  • The current item number, identified by the # symbol. +
      +
    • This simply counts the note and is affected by sorting.
    • +
    +
  • +
  • Note ID, + representing the unique ID used internally by Trilium
  • +
  • The title of the note.
  • +

Interaction

Creating a new table

Right click the Note Tree and select Insert child note and look for the Table item.

Adding columns

-

Each column is a promoted attribute that - is defined on the Collection note. Ideally, the promoted attributes need - to be inheritable in order to show up in the child notes.

-

To create a new column, simply press Add new column at the bottom - of the table.

-

There are also a few predefined columns:

+

Each column is a promoted or unpromoted attribute that + is defined on the Collection note.

+

To create a new column, either:

    -
  • The current item number, identified by the # symbol. This simply - counts the note and is affected by sorting.
  • -
  • Note ID, - representing the unique ID used internally by Trilium
  • -
  • The title of the note.
  • +
  • Press Add new column at the bottom of the table.
  • +
  • Right click on an existing column and select Add column to the left/right.
  • +
  • Right click on the empty space of the column header and select New column.

Adding new rows

Each row is actually a note that is a child of the Collection note.

-

To create a new note, press Add new row at the bottom of the table. - By default it will try to edit the title of the newly created note.

-

Alternatively, the note can be created from theTo create a new note, either:

+
    +
  • Press Add new row at the bottom of the table.
  • +
  • Right click on an existing row and select Insert row above, Insert child note or Insert row below.
  • +
+

By default it will try to edit the title of the newly created note.

+

Alternatively, the note can be created from the Note Tree or scripting.

+

Context menu

+

There are multiple menus:

+
    +
  • Right clicking on a column, allows: +
      +
    • Sorting by the selected column and resetting the sort.
    • +
    • Hiding the selected column or adjusting the visibility of every column.
    • +
    • Adding new columns to the left or the right of the column.
    • +
    • Editing the current column.
    • +
    • Deleting the current column.
    • +
    +
  • +
  • Right clicking on the space to the right of the columns, allows: +
      +
    • Adjusting the visibility of every column.
    • +
    • Adding new columns.
    • +
    +
  • +
  • Right clicking on a row, allows: +
      +
    • Opening the corresponding note of the row in a new tab, split, window + or quick editing it.
    • +
    • Inserting rows above, below or as a child note.
    • +
    • Deleting the row.
    • +
    +
  • +

Editing data

Simply click on a cell within a row to change its value. The change will not only reflect in the table, but also as an attribute of the corresponding note.

    -
  • The editing will respect the type of the promoted attribute, by presenting +
  • The editing will respect the type of the promoted attribute, by presenting a normal text box, a number selector or a date selector for example.
  • -
  • It also possible to change the title of a note.
  • -
  • Editing relations is also possible, by using the note autocomplete.
  • +
  • It also possible to change the title of a note.
  • +
  • Editing relations is also possible +
      +
    • Simply click on a relation and it will become editable. Enter the text + to look for a note and click on it.
    • +
    • To remove a relation, remove the title of the note from the text box and + press Enter.
    • +
    +
+

Editing columns

+

It is possible to edit a column by right clicking it and selecting Edit column. This + will basically change the label/relation definition at the collection level.

+

If the Name field of a column is changed, this will trigger a batch + operation in which the corresponding label/relation will be renamed in + all the children.

Working with the data

-

Sorting

-

It is possible to sort the data by the values of a column:

+

Sorting by column

+

By default, the order of the notes matches the order in the Note Tree. + However, it is possible to sort the data by the values of a column:

    -
  • To do so, simply click on a column.
  • -
  • To switch between ascending or descending sort, simply click again on +
  • To do so, simply click on a column.
  • +
  • To switch between ascending or descending sort, simply click again on the same column. The arrow next to the column will indicate the direction of the sort.
  • +
  • To disable sorting and fall back to the original order, right click any + column on the header and select Clear sorting. +

Reordering and hiding columns

    -
  • Columns can be reordered by dragging the header of the columns.
  • -
  • Columns can be hidden or shown by right clicking on a column and clicking +
  • Columns can be reordered by dragging the header of the columns.
  • +
  • Columns can be hidden or shown by right clicking on a column and clicking the item corresponding to the column.

Reordering rows

-

Notes can be dragged around to change their order. This will also change - the order of the note in the Note Tree.

-

Currently, it's possible to reorder notes even if sorting is used, but - the result might be inconsistent.

-

Limitations

-

The table functionality is still in its early stages, as such it faces - quite a few important limitations:

-
    -
  1. As mentioned previously, the columns of the table are defined as  - Promoted Attributes. -
      -
    1. But only the promoted attributes that are defined at the level of the - Collection note are actually taken into consideration.
    2. -
    3. There are plans to recursively look for columns across the sub-hierarchy.
    4. -
    +

    Notes can be dragged around to change their order. To do so, move the + mouse over the three vertical dots near the number row and drag the mouse + to the desired position.

    +

    This will also change the order of the note in the Note Tree.

    +

    Reordering does have some limitations:

    +
      +
    • If the parent note has #sorted, reordering will be disabled.
    • +
    • If using nested tables, then reordering will also be disabled.
    • +
    • Currently, it's possible to reorder notes even if column sorting is used, + but the result might be inconsistent.
    • +
    +

    Nested trees

    +

    If the child notes of the collection also have their own child notes, + then they will be displayed in a hierarchy.

    +

    Next to the title of each element there will be a button to expand or + collapse. By default, all items are expanded.

    +

    Since nesting is not always desirable, it is possible to limit the nesting + to a certain number of levels or even disable it completely. To do so, + either:

    +
      +
    • Go to Collection Properties in the Ribbon and + look for the Max nesting depth section. +
        +
      • To disable nesting, type 0 and press Enter.
      • +
      • To limit to a certain depth, type in the desired number (e.g. 2 to only + display children and sub-children).
      • +
      • To re-enable unlimited nesting, remove the number and press Enter.
      • +
    • -
    • Hierarchy is not yet supported, so the table will only show the items - that are direct children of the Collection note.
    • -
    • Multiple labels and relations are not supported. If a Promoted Attributes is defined - with a Multi value specificity, they will be ignored.
    • -
+
  • Manually set maxNestingDepth to the desired value.
  • + +

    Limitations:

    +
      +
    • While in this mode, it's not possible to reorder notes.
    • +
    +

    Limitations

    +

    Multi-value labels and relations are not supported. If a Promoted Attributes is defined + with a Multi value specificity, they will be ignored.

    Use in search

    The table view can be used in a Saved Search by adding the #viewType=table attribute.

    @@ -86,8 +183,8 @@ of the Search.

    However, there are also some limitations:

      -
    • It's not possible to reorder notes.
    • -
    • It's not possible to add a new row.
    • +
    • It's not possible to reorder notes.
    • +
    • It's not possible to add a new row.

    Columns are supported, by being defined as Promoted Attributes to the  diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json index 35ab128cb..3792a0457 100644 --- a/docs/User Guide/!!!meta.json +++ b/docs/User Guide/!!!meta.json @@ -3739,13 +3739,6 @@ "isInheritable": false, "position": 10 }, - { - "type": "relation", - "name": "internalLink", - "value": "m1lbrzyKDaRB", - "isInheritable": false, - "position": 20 - }, { "type": "relation", "name": "internalLink", @@ -3780,6 +3773,20 @@ "value": "bx bx-table", "isInheritable": false, "position": 10 + }, + { + "type": "relation", + "name": "internalLink", + "value": "m1lbrzyKDaRB", + "isInheritable": false, + "position": 70 + }, + { + "type": "relation", + "name": "internalLink", + "value": "BlN9DFI679QC", + "isInheritable": false, + "position": 80 } ], "format": "markdown", diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md index dadae0183..b74047cd2 100644 --- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md +++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md @@ -3,6 +3,24 @@ The table view displays information in a grid, where the rows are individual notes and the columns are Promoted Attributes. In addition, values are editable. +## How it works + +The tabular structure is represented as such: + +* Each child note is a row in the table. +* If child rows also have children, they will be displayed under an expander (nested notes). +* Each column is a [promoted attribute](../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md) that is defined on the Collection note. + * Actually, both promoted and unpromoted attributes are supported, but it's a requirement to use a label/relation definition. + * The promoted attributes are usually defined as inheritable in order to show up in the child notes, but it's not a requirement. +* If there are multiple attribute definitions with the same `name`, only one will be displayed. + +There are also a few predefined columns: + +* The current item number, identified by the `#` symbol. + * This simply counts the note and is affected by sorting. +* Note ID, representing the unique ID used internally by Trilium +* The title of the note. + ## Interaction ### Creating a new table @@ -11,23 +29,44 @@ Right click the Note ID, representing the unique ID used internally by Trilium -* The title of the note. +* Press _Add new column_ at the bottom of the table. +* Right click on an existing column and select Add column to the left/right. +* Right click on the empty space of the column header and select _New column_. ### Adding new rows Each row is actually a note that is a child of the Collection note. -To create a new note, press _Add new row_ at the bottom of the table. By default it will try to edit the title of the newly created note. +To create a new note, either: -Alternatively, the note can be created from theNote Tree or [scripting](../../../Scripting.md). +* Press _Add new row_ at the bottom of the table. +* Right click on an existing row and select _Insert row above, Insert child note_ or _Insert row below_. + +By default it will try to edit the title of the newly created note. + +Alternatively, the note can be created from the Note Tree or [scripting](../../../Scripting.md). + +### Context menu + +There are multiple menus: + +* Right clicking on a column, allows: + * Sorting by the selected column and resetting the sort. + * Hiding the selected column or adjusting the visibility of every column. + * Adding new columns to the left or the right of the column. + * Editing the current column. + * Deleting the current column. +* Right clicking on the space to the right of the columns, allows: + * Adjusting the visibility of every column. + * Adding new columns. +* Right clicking on a row, allows: + * Opening the corresponding note of the row in a new tab, split, window or quick editing it. + * Inserting rows above, below or as a child note. + * Deleting the row. ### Editing data @@ -35,16 +74,25 @@ Simply click on a cell within a row to change its value. The change will not onl * The editing will respect the type of the promoted attribute, by presenting a normal text box, a number selector or a date selector for example. * It also possible to change the title of a note. -* Editing relations is also possible, by using the note autocomplete. +* Editing relations is also possible + * Simply click on a relation and it will become editable. Enter the text to look for a note and click on it. + * To remove a relation, remove the title of the note from the text box and press Enter. + +### Editing columns + +It is possible to edit a column by right clicking it and selecting _Edit column._ This will basically change the label/relation definition at the collection level. + +If the _Name_ field of a column is changed, this will trigger a batch operation in which the corresponding label/relation will be renamed in all the children. ## Working with the data -### Sorting +### Sorting by column -It is possible to sort the data by the values of a column: +By default, the order of the notes matches the order in the Note Tree. However, it is possible to sort the data by the values of a column: * To do so, simply click on a column. * To switch between ascending or descending sort, simply click again on the same column. The arrow next to the column will indicate the direction of the sort. +* To disable sorting and fall back to the original order, right click any column on the header and select _Clear sorting._ ### Reordering and hiding columns @@ -53,19 +101,37 @@ It is possible to sort the data by the values of a column: ### Reordering rows -Notes can be dragged around to change their order. This will also change the order of the note in the Note Tree. +Notes can be dragged around to change their order. To do so, move the mouse over the three vertical dots near the number row and drag the mouse to the desired position. -Currently, it's possible to reorder notes even if sorting is used, but the result might be inconsistent. +This will also change the order of the note in the Note Tree. + +Reordering does have some limitations: + +* If the parent note has `#sorted`, reordering will be disabled. +* If using nested tables, then reordering will also be disabled. +* Currently, it's possible to reorder notes even if column sorting is used, but the result might be inconsistent. + +### Nested trees + +If the child notes of the collection also have their own child notes, then they will be displayed in a hierarchy. + +Next to the title of each element there will be a button to expand or collapse. By default, all items are expanded. + +Since nesting is not always desirable, it is possible to limit the nesting to a certain number of levels or even disable it completely. To do so, either: + +* Go to _Collection Properties_ in the Ribbon and look for the _Max nesting depth_ section. + * To disable nesting, type 0 and press Enter. + * To limit to a certain depth, type in the desired number (e.g. 2 to only display children and sub-children). + * To re-enable unlimited nesting, remove the number and press Enter. +* Manually set `maxNestingDepth` to the desired value. + +Limitations: + +* While in this mode, it's not possible to reorder notes. ## Limitations -The table functionality is still in its early stages, as such it faces quite a few important limitations: - -1. As mentioned previously, the columns of the table are defined as Promoted Attributes. - 1. But only the promoted attributes that are defined at the level of the Collection note are actually taken into consideration. - 2. There are plans to recursively look for columns across the sub-hierarchy. -2. Hierarchy is not yet supported, so the table will only show the items that are direct children of the _Collection_ note. -3. Multiple labels and relations are not supported. If a Promoted Attributes is defined with a _Multi value_ specificity, they will be ignored. +Multi-value labels and relations are not supported. If a Promoted Attributes is defined with a _Multi value_ specificity, they will be ignored. ## Use in search From daa4743967a04de02b36e8897349ea7918a9ef6d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:15:33 +0300 Subject: [PATCH 04/17] refactor(server): add some type safety to bulk actions --- apps/server/src/services/bulk_actions.ts | 82 ++++++++++++++++++------ 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 1c57c6cd5..25f22ba51 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -6,53 +6,97 @@ import { randomString } from "./utils.js"; import eraseService from "./erase.js"; import type BNote from "../becca/entities/bnote.js"; -interface Action { +interface AddLabelAction { labelName: string; - labelValue: string; + labelValue?: string; +} + +interface AddRelationAction { + relationName: string; + targetNoteId: string; +} + +interface DeleteRevisionsAction {} +interface DeleteLabelAction { + labelName: string; +} + +interface DeleteRelationAction { + relationName: string; +} + +interface RenameNoteAction { + newTitle: string; +} + +interface RenameLabelAction { oldLabelName: string; newLabelName: string; +} - relationName: string; +interface RenameRelationAction { oldRelationName: string; newRelationName: string; +} +interface UpdateLabelValueAction { + labelName: string; + labelValue: string; +} + +interface UpdateRelationTargetAction { + relationName: string; targetNoteId: string; +} + +interface MoveNoteAction { targetParentNoteId: string; - newTitle: string; +} + +interface ExecuteScriptAction { script: string; } -type ActionHandler = (action: Action, note: BNote) => void; -const ACTION_HANDLERS: Record = { - addLabel: (action, note) => { +interface DeleteNoteAction { } + +type BulkAction = AddLabelAction | AddRelationAction | DeleteNoteAction | DeleteRevisionsAction | DeleteLabelAction | DeleteRelationAction | RenameNoteAction | RenameLabelAction | RenameRelationAction | UpdateLabelValueAction | UpdateRelationTargetAction | MoveNoteAction | ExecuteScriptAction; + +type ActionHandler = (action: T, note: BNote) => void; + +type ActionHandlerMap = { + [K in keyof BulkAction]: ActionHandler; +} + +const ACTION_HANDLERS: ActionHandlerMap = { + addLabel: (action: AddLabelAction, note: BNote) => { note.addLabel(action.labelName, action.labelValue); }, - addRelation: (action, note) => { + addRelation: (action: AddRelationAction, note: BNote) => { note.addRelation(action.relationName, action.targetNoteId); }, - deleteNote: (action, note) => { + deleteNote: (action: DeleteNoteAction, note: BNote) => { const deleteId = `searchbulkaction-${randomString(10)}`; note.deleteNote(deleteId); }, - deleteRevisions: (action, note) => { + deleteRevisions: (action: DeleteRevisionsAction, note: BNote) => { const revisionIds = note .getRevisions() .map((rev) => rev.revisionId) .filter((rev) => !!rev) as string[]; eraseService.eraseRevisions(revisionIds); }, - deleteLabel: (action, note) => { + deleteLabel: (action: DeleteLabelAction, note: BNote) => { for (const label of note.getOwnedLabels(action.labelName)) { label.markAsDeleted(); } }, - deleteRelation: (action, note) => { + deleteRelation: (action: DeleteRelationAction, note: BNote) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.markAsDeleted(); } }, - renameNote: (action, note) => { + renameNote: (action: RenameNoteAction, note: BNote) => { // "officially" injected value: // - note @@ -63,7 +107,7 @@ const ACTION_HANDLERS: Record = { note.save(); } }, - renameLabel: (action, note) => { + renameLabel: (action: RenameLabelAction, note: BNote) => { for (const label of note.getOwnedLabels(action.oldLabelName)) { // attribute name is immutable, renaming means delete old + create new const newLabel = label.createClone("label", action.newLabelName, label.value); @@ -72,7 +116,7 @@ const ACTION_HANDLERS: Record = { label.markAsDeleted(); } }, - renameRelation: (action, note) => { + renameRelation: (action: RenameRelationAction, note: BNote) => { for (const relation of note.getOwnedRelations(action.oldRelationName)) { // attribute name is immutable, renaming means delete old + create new const newRelation = relation.createClone("relation", action.newRelationName, relation.value); @@ -81,19 +125,19 @@ const ACTION_HANDLERS: Record = { relation.markAsDeleted(); } }, - updateLabelValue: (action, note) => { + updateLabelValue: (action: UpdateLabelValueAction, note: BNote) => { for (const label of note.getOwnedLabels(action.labelName)) { label.value = action.labelValue; label.save(); } }, - updateRelationTarget: (action, note) => { + updateRelationTarget: (action: UpdateRelationTargetAction, note: BNote) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.value = action.targetNoteId; relation.save(); } }, - moveNote: (action, note) => { + moveNote: (action: MoveNoteAction, note: BNote) => { const targetParentNote = becca.getNote(action.targetParentNoteId); if (!targetParentNote) { @@ -114,7 +158,7 @@ const ACTION_HANDLERS: Record = { log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); } }, - executeScript: (action, note) => { + executeScript: (action: ExecuteScriptAction, note: BNote) => { if (!action.script || !action.script.trim()) { log.info("Ignoring executeScript since the script is empty."); return; From e2c84437784fe1ed52f0c645a45a92088a5b3899 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:32:47 +0300 Subject: [PATCH 05/17] refactor(bulk_action): remake types & change method signature --- apps/server/src/routes/api/bulk_action.ts | 2 +- apps/server/src/routes/api/search.ts | 2 +- apps/server/src/services/bulk_actions.ts | 148 +++++++++++----------- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/apps/server/src/routes/api/bulk_action.ts b/apps/server/src/routes/api/bulk_action.ts index 9a3a9ce1c..6353322ea 100644 --- a/apps/server/src/routes/api/bulk_action.ts +++ b/apps/server/src/routes/api/bulk_action.ts @@ -12,7 +12,7 @@ function execute(req: Request) { const bulkActionNote = becca.getNoteOrThrow("_bulkAction"); - bulkActionService.executeActions(bulkActionNote, affectedNoteIds); + bulkActionService.executeActionsFromNote(bulkActionNote, affectedNoteIds); } function getAffectedNoteCount(req: Request) { diff --git a/apps/server/src/routes/api/search.ts b/apps/server/src/routes/api/search.ts index 31627ba0a..0e9304a07 100644 --- a/apps/server/src/routes/api/search.ts +++ b/apps/server/src/routes/api/search.ts @@ -40,7 +40,7 @@ function searchAndExecute(req: Request) { const { searchResultNoteIds } = searchService.searchFromNote(note); - bulkActionService.executeActions(note, searchResultNoteIds); + bulkActionService.executeActionsFromNote(note, searchResultNoteIds); } function quickSearch(req: Request) { diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 25f22ba51..a1447c858 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -6,97 +6,88 @@ import { randomString } from "./utils.js"; import eraseService from "./erase.js"; import type BNote from "../becca/entities/bnote.js"; -interface AddLabelAction { - labelName: string; - labelValue?: string; -} - -interface AddRelationAction { - relationName: string; - targetNoteId: string; -} - -interface DeleteRevisionsAction {} -interface DeleteLabelAction { - labelName: string; -} - -interface DeleteRelationAction { - relationName: string; -} - -interface RenameNoteAction { - newTitle: string; -} - -interface RenameLabelAction { - oldLabelName: string; - newLabelName: string; -} - -interface RenameRelationAction { - oldRelationName: string; - newRelationName: string; -} - -interface UpdateLabelValueAction { - labelName: string; - labelValue: string; -} - -interface UpdateRelationTargetAction { - relationName: string; - targetNoteId: string; -} - -interface MoveNoteAction { - targetParentNoteId: string; -} - -interface ExecuteScriptAction { - script: string; -} - -interface DeleteNoteAction { } - -type BulkAction = AddLabelAction | AddRelationAction | DeleteNoteAction | DeleteRevisionsAction | DeleteLabelAction | DeleteRelationAction | RenameNoteAction | RenameLabelAction | RenameRelationAction | UpdateLabelValueAction | UpdateRelationTargetAction | MoveNoteAction | ExecuteScriptAction; +type ActionHandlers = { + addLabel: { + labelName: string; + labelValue?: string; + }, + addRelation: { + relationName: string; + targetNoteId: string; + }, + deleteNote: {}, + deleteRevisions: {}, + deleteLabel: { + labelName: string; + }, + deleteRelation: { + relationName: string; + }, + renameNote: { + newTitle: string; + }, + renameLabel: { + oldLabelName: string; + newLabelName: string; + }, + renameRelation: { + oldRelationName: string; + newRelationName: string; + }, + updateLabelValue: { + labelName: string; + labelValue: string; + }, + updateRelationTarget: { + relationName: string; + targetNoteId: string; + }, + moveNote: { + targetParentNoteId: string; + }, + executeScript: { + script: string; + } +}; type ActionHandler = (action: T, note: BNote) => void; type ActionHandlerMap = { - [K in keyof BulkAction]: ActionHandler; -} + [K in keyof ActionHandlers]: ActionHandler; +}; + +export type BulkAction = { name: keyof ActionHandlers } & ActionHandlers[keyof ActionHandlers]; const ACTION_HANDLERS: ActionHandlerMap = { - addLabel: (action: AddLabelAction, note: BNote) => { + addLabel: (action, note) => { note.addLabel(action.labelName, action.labelValue); }, - addRelation: (action: AddRelationAction, note: BNote) => { + addRelation: (action, note) => { note.addRelation(action.relationName, action.targetNoteId); }, - deleteNote: (action: DeleteNoteAction, note: BNote) => { + deleteNote: (action, note) => { const deleteId = `searchbulkaction-${randomString(10)}`; note.deleteNote(deleteId); }, - deleteRevisions: (action: DeleteRevisionsAction, note: BNote) => { + deleteRevisions: (action, note) => { const revisionIds = note .getRevisions() .map((rev) => rev.revisionId) .filter((rev) => !!rev) as string[]; eraseService.eraseRevisions(revisionIds); }, - deleteLabel: (action: DeleteLabelAction, note: BNote) => { + deleteLabel: (action, note) => { for (const label of note.getOwnedLabels(action.labelName)) { label.markAsDeleted(); } }, - deleteRelation: (action: DeleteRelationAction, note: BNote) => { + deleteRelation: (action, note) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.markAsDeleted(); } }, - renameNote: (action: RenameNoteAction, note: BNote) => { + renameNote: (action, note) => { // "officially" injected value: // - note @@ -107,7 +98,7 @@ const ACTION_HANDLERS: ActionHandlerMap = { note.save(); } }, - renameLabel: (action: RenameLabelAction, note: BNote) => { + renameLabel: (action, note) => { for (const label of note.getOwnedLabels(action.oldLabelName)) { // attribute name is immutable, renaming means delete old + create new const newLabel = label.createClone("label", action.newLabelName, label.value); @@ -116,7 +107,7 @@ const ACTION_HANDLERS: ActionHandlerMap = { label.markAsDeleted(); } }, - renameRelation: (action: RenameRelationAction, note: BNote) => { + renameRelation: (action, note) => { for (const relation of note.getOwnedRelations(action.oldRelationName)) { // attribute name is immutable, renaming means delete old + create new const newRelation = relation.createClone("relation", action.newRelationName, relation.value); @@ -125,19 +116,19 @@ const ACTION_HANDLERS: ActionHandlerMap = { relation.markAsDeleted(); } }, - updateLabelValue: (action: UpdateLabelValueAction, note: BNote) => { + updateLabelValue: (action, note) => { for (const label of note.getOwnedLabels(action.labelName)) { label.value = action.labelValue; label.save(); } }, - updateRelationTarget: (action: UpdateRelationTargetAction, note: BNote) => { + updateRelationTarget: (action, note) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.value = action.targetNoteId; relation.save(); } }, - moveNote: (action: MoveNoteAction, note: BNote) => { + moveNote: (action, note) => { const targetParentNote = becca.getNote(action.targetParentNoteId); if (!targetParentNote) { @@ -158,7 +149,7 @@ const ACTION_HANDLERS: ActionHandlerMap = { log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); } }, - executeScript: (action: ExecuteScriptAction, note: BNote) => { + executeScript: (action, note) => { if (!action.script || !action.script.trim()) { log.info("Ignoring executeScript since the script is empty."); return; @@ -169,7 +160,7 @@ const ACTION_HANDLERS: ActionHandlerMap = { note.save(); } -}; +} as const; function getActions(note: BNote) { return note @@ -189,15 +180,23 @@ function getActions(note: BNote) { return null; } - return action; + return action as BulkAction; }) .filter((a) => !!a); } -function executeActions(note: BNote, searchResultNoteIds: string[] | Set) { +/** + * Executes the bulk actions defined in the note against the provided search result note IDs. + * @param note the note containing the bulk actions, read from the `action` label. + * @param noteIds the IDs of the notes to apply the actions to. + */ +function executeActionsFromNote(note: BNote, noteIds: string[] | Set) { const actions = getActions(note); + return executeActions(actions, noteIds); +} - for (const resultNoteId of searchResultNoteIds) { +function executeActions(actions: BulkAction[], noteIds: string[] | Set) { + for (const resultNoteId of noteIds) { const resultNote = becca.getNote(resultNoteId); if (!resultNote) { @@ -217,5 +216,6 @@ function executeActions(note: BNote, searchResultNoteIds: string[] | Set } export default { - executeActions + executeActions, + executeActionsFromNote }; From 5d619131ec30ae49ec0bf2b75b0bd594a5296c76 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:44:55 +0300 Subject: [PATCH 06/17] fix(views/table): bulk actions sometimes not working --- .../widgets/view_widgets/table_view/bulk_actions.ts | 12 ++---------- apps/server/src/routes/api/bulk_action.ts | 13 +++++++++++-- apps/server/src/services/bulk_actions.ts | 10 +++++++--- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts index 48262b582..e4f24f4ed 100644 --- a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts +++ b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts @@ -1,6 +1,4 @@ import { t } from "i18next"; -import attributes from "../../../services/attributes"; -import froca from "../../../services/froca"; import server from "../../../services/server"; import toast from "../../../services/toast"; import ws from "../../../services/ws"; @@ -36,16 +34,10 @@ export async function deleteColumn(parentNoteId: string, type: "label" | "relati } async function executeBulkAction(parentNoteId: string, action: {}) { - const bulkActionNote = await froca.getNote("_bulkAction"); - if (!bulkActionNote) { - console.warn("Bulk action note not found"); - return; - } - - attributes.setLabel("_bulkAction", "action", JSON.stringify(action)); await server.post("bulk-action/execute", { noteIds: [ parentNoteId ], - includeDescendants: true + includeDescendants: true, + actions: [ action ] }); await ws.waitForMaxKnownEntityChangeId(); diff --git a/apps/server/src/routes/api/bulk_action.ts b/apps/server/src/routes/api/bulk_action.ts index 6353322ea..d76ec43ea 100644 --- a/apps/server/src/routes/api/bulk_action.ts +++ b/apps/server/src/routes/api/bulk_action.ts @@ -3,7 +3,7 @@ import becca from "../../becca/becca.js"; import bulkActionService from "../../services/bulk_actions.js"; function execute(req: Request) { - const { noteIds, includeDescendants } = req.body; + const { noteIds, includeDescendants, actions } = req.body; if (!Array.isArray(noteIds)) { throw new Error("noteIds must be an array"); } @@ -12,7 +12,16 @@ function execute(req: Request) { const bulkActionNote = becca.getNoteOrThrow("_bulkAction"); - bulkActionService.executeActionsFromNote(bulkActionNote, affectedNoteIds); + if (actions && actions.length > 0) { + for (const action of actions) { + if (!action.name) { + throw new Error("Action must have a name"); + } + } + bulkActionService.executeActions(actions, affectedNoteIds); + } else { + bulkActionService.executeActionsFromNote(bulkActionNote, affectedNoteIds); + } } function getAffectedNoteCount(req: Request) { diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index a1447c858..71333992c 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -50,13 +50,15 @@ type ActionHandlers = { } }; +type BulkActionData = ActionHandlers[T] & { name: T }; + type ActionHandler = (action: T, note: BNote) => void; type ActionHandlerMap = { - [K in keyof ActionHandlers]: ActionHandler; + [K in keyof ActionHandlers]: ActionHandler>; }; -export type BulkAction = { name: keyof ActionHandlers } & ActionHandlers[keyof ActionHandlers]; +export type BulkAction = BulkActionData; const ACTION_HANDLERS: ActionHandlerMap = { addLabel: (action, note) => { @@ -207,7 +209,9 @@ function executeActions(actions: BulkAction[], noteIds: string[] | Set) try { log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); - ACTION_HANDLERS[action.name](action, resultNote); + const handler = ACTION_HANDLERS[action.name]; + //@ts-ignore + handler(action as BulkAction, resultNote); } catch (e: any) { log.error(`ExecuteScript search action failed with ${e.message}`); } From 0d3de928900f5dd92f96c68ae2eb5d8fdde15952 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:46:38 +0300 Subject: [PATCH 07/17] refactor(views/table): move bulk action implementation in service --- apps/client/src/services/bulk_action.ts | 12 +++++++ .../view_widgets/table_view/bulk_actions.ts | 32 ++++++------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/apps/client/src/services/bulk_action.ts b/apps/client/src/services/bulk_action.ts index 66922ef62..c4441259f 100644 --- a/apps/client/src/services/bulk_action.ts +++ b/apps/client/src/services/bulk_action.ts @@ -15,6 +15,7 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import { t } from "./i18n.js"; import type FNote from "../entities/fnote.js"; +import toast from "./toast.js"; const ACTION_GROUPS = [ { @@ -89,6 +90,17 @@ function parseActions(note: FNote) { .filter((action) => !!action); } +export async function executeBulkActions(parentNoteId: string, actions: {}[]) { + await server.post("bulk-action/execute", { + noteIds: [ parentNoteId ], + includeDescendants: true, + actions + }); + + await ws.waitForMaxKnownEntityChangeId(); + toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); +} + export default { addAction, parseActions, diff --git a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts index e4f24f4ed..b0f590c16 100644 --- a/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts +++ b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts @@ -1,45 +1,31 @@ -import { t } from "i18next"; -import server from "../../../services/server"; -import toast from "../../../services/toast"; -import ws from "../../../services/ws"; +import { executeBulkActions } from "../../../services/bulk_action.js"; export async function renameColumn(parentNoteId: string, type: "label" | "relation", originalName: string, newName: string) { if (type === "label") { - return executeBulkAction(parentNoteId, { + return executeBulkActions(parentNoteId, [{ name: "renameLabel", oldLabelName: originalName, newLabelName: newName - }); + }]); } else { - return executeBulkAction(parentNoteId, { + return executeBulkActions(parentNoteId, [{ name: "renameRelation", oldRelationName: originalName, newRelationName: newName - }); + }]); } } export async function deleteColumn(parentNoteId: string, type: "label" | "relation", columnName: string) { if (type === "label") { - return executeBulkAction(parentNoteId, { + return executeBulkActions(parentNoteId, [{ name: "deleteLabel", labelName: columnName - }); + }]); } else { - return executeBulkAction(parentNoteId, { + return executeBulkActions(parentNoteId, [{ name: "deleteRelation", relationName: columnName - }); + }]); } } - -async function executeBulkAction(parentNoteId: string, action: {}) { - await server.post("bulk-action/execute", { - noteIds: [ parentNoteId ], - includeDescendants: true, - actions: [ action ] - }); - - await ws.waitForMaxKnownEntityChangeId(); - toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); -} From 409638151c270e2bfcfc3253e1111df3308ba69b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:54:16 +0300 Subject: [PATCH 08/17] refactor(bulk_action): add basic type safety for client --- apps/client/src/services/bulk_action.ts | 3 +- apps/server/src/services/bulk_actions.ts | 49 +----------------------- packages/commons/src/index.ts | 1 + packages/commons/src/lib/bulk_actions.ts | 47 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 49 deletions(-) create mode 100644 packages/commons/src/lib/bulk_actions.ts diff --git a/apps/client/src/services/bulk_action.ts b/apps/client/src/services/bulk_action.ts index c4441259f..57b880c36 100644 --- a/apps/client/src/services/bulk_action.ts +++ b/apps/client/src/services/bulk_action.ts @@ -16,6 +16,7 @@ import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import { t } from "./i18n.js"; import type FNote from "../entities/fnote.js"; import toast from "./toast.js"; +import { BulkAction } from "@triliumnext/commons"; const ACTION_GROUPS = [ { @@ -90,7 +91,7 @@ function parseActions(note: FNote) { .filter((action) => !!action); } -export async function executeBulkActions(parentNoteId: string, actions: {}[]) { +export async function executeBulkActions(parentNoteId: string, actions: BulkAction[]) { await server.post("bulk-action/execute", { noteIds: [ parentNoteId ], includeDescendants: true, diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 71333992c..5b2574156 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -5,52 +5,7 @@ import branchService from "./branches.js"; import { randomString } from "./utils.js"; import eraseService from "./erase.js"; import type BNote from "../becca/entities/bnote.js"; - -type ActionHandlers = { - addLabel: { - labelName: string; - labelValue?: string; - }, - addRelation: { - relationName: string; - targetNoteId: string; - }, - deleteNote: {}, - deleteRevisions: {}, - deleteLabel: { - labelName: string; - }, - deleteRelation: { - relationName: string; - }, - renameNote: { - newTitle: string; - }, - renameLabel: { - oldLabelName: string; - newLabelName: string; - }, - renameRelation: { - oldRelationName: string; - newRelationName: string; - }, - updateLabelValue: { - labelName: string; - labelValue: string; - }, - updateRelationTarget: { - relationName: string; - targetNoteId: string; - }, - moveNote: { - targetParentNoteId: string; - }, - executeScript: { - script: string; - } -}; - -type BulkActionData = ActionHandlers[T] & { name: T }; +import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons"; type ActionHandler = (action: T, note: BNote) => void; @@ -58,8 +13,6 @@ type ActionHandlerMap = { [K in keyof ActionHandlers]: ActionHandler>; }; -export type BulkAction = BulkActionData; - const ACTION_HANDLERS: ActionHandlerMap = { addLabel: (action, note) => { note.addLabel(action.labelName, action.labelValue); diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 96ba3325f..5340e06d6 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -5,3 +5,4 @@ export * from "./lib/hidden_subtree.js"; export * from "./lib/rows.js"; export * from "./lib/test-utils.js"; export * from "./lib/mime_type.js"; +export * from "./lib/bulk_actions.js"; diff --git a/packages/commons/src/lib/bulk_actions.ts b/packages/commons/src/lib/bulk_actions.ts new file mode 100644 index 000000000..4dbce561e --- /dev/null +++ b/packages/commons/src/lib/bulk_actions.ts @@ -0,0 +1,47 @@ +export type ActionHandlers = { + addLabel: { + labelName: string; + labelValue?: string; + }, + addRelation: { + relationName: string; + targetNoteId: string; + }, + deleteNote: {}, + deleteRevisions: {}, + deleteLabel: { + labelName: string; + }, + deleteRelation: { + relationName: string; + }, + renameNote: { + newTitle: string; + }, + renameLabel: { + oldLabelName: string; + newLabelName: string; + }, + renameRelation: { + oldRelationName: string; + newRelationName: string; + }, + updateLabelValue: { + labelName: string; + labelValue: string; + }, + updateRelationTarget: { + relationName: string; + targetNoteId: string; + }, + moveNote: { + targetParentNoteId: string; + }, + executeScript: { + script: string; + } +}; + +export type BulkActionData = ActionHandlers[T] & { name: T }; + +export type BulkAction = BulkActionData; From 94dad49e2f542b0bd37ac4b5888081a2c0de68cb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:56:37 +0300 Subject: [PATCH 09/17] refactor(bulk_action): full type safety for client --- packages/commons/src/lib/bulk_actions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/commons/src/lib/bulk_actions.ts b/packages/commons/src/lib/bulk_actions.ts index 4dbce561e..d80c18974 100644 --- a/packages/commons/src/lib/bulk_actions.ts +++ b/packages/commons/src/lib/bulk_actions.ts @@ -44,4 +44,6 @@ export type ActionHandlers = { export type BulkActionData = ActionHandlers[T] & { name: T }; -export type BulkAction = BulkActionData; +export type BulkAction = { + [K in keyof ActionHandlers]: { name: K; } & ActionHandlers[K]; +}[keyof ActionHandlers]; From 8f393d0baed99da372b70f0b51d33128582af17e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 12:57:58 +0300 Subject: [PATCH 10/17] refactor(bulk_action): fix type error --- apps/server/src/services/bulk_actions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 5b2574156..531bd9976 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -162,9 +162,8 @@ function executeActions(actions: BulkAction[], noteIds: string[] | Set) try { log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); - const handler = ACTION_HANDLERS[action.name]; - //@ts-ignore - handler(action as BulkAction, resultNote); + const handler = ACTION_HANDLERS[action.name] as (a: typeof action, n: BNote) => void; + handler(action, resultNote); } catch (e: any) { log.error(`ExecuteScript search action failed with ${e.message}`); } From 40a5eee211202dc475ef1be0e9354f00a76b4340 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 13:00:08 +0300 Subject: [PATCH 11/17] docs(views/table): describe exactly how to remove relation --- .../Basic Concepts and Features/Notes/Note List/Table View.html | 2 +- .../Basic Concepts and Features/Notes/Note List/Table View.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html index 6a78bf2cc..92c9f3e7c 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html @@ -100,7 +100,7 @@

  • Simply click on a relation and it will become editable. Enter the text to look for a note and click on it.
  • To remove a relation, remove the title of the note from the text box and - press Enter.
  • + click outside the cell. diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md index b74047cd2..d2a59b3ed 100644 --- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md +++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md @@ -76,7 +76,7 @@ Simply click on a cell within a row to change its value. The change will not onl * It also possible to change the title of a note. * Editing relations is also possible * Simply click on a relation and it will become editable. Enter the text to look for a note and click on it. - * To remove a relation, remove the title of the note from the text box and press Enter. + * To remove a relation, remove the title of the note from the text box and click outside the cell. ### Editing columns From beb1c15fa51114d2a3e0943707644cccf7056a8e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 13:25:54 +0300 Subject: [PATCH 12/17] fix(views/table): inheritable checkbox not respected --- apps/client/src/services/attributes.ts | 5 +++-- .../src/widgets/view_widgets/table_view/col_editing.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 52ea8967a..370fd1ce1 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -12,11 +12,12 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe }); } -export async function setLabel(noteId: string, name: string, value: string = "") { +export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) { await server.put(`notes/${noteId}/set-attribute`, { type: "label", name: name, - value: value + value: value, + isInheritable }); } diff --git a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts index ac8c7e740..cb3f90bc9 100644 --- a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts +++ b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts @@ -45,7 +45,8 @@ export default class TableColumnEditing extends Component { attr = { type: "label", name: `${type ?? "label"}:myLabel`, - value: "promoted,single,text" + value: "promoted,single,text", + isInheritable: true }; } @@ -78,7 +79,7 @@ export default class TableColumnEditing extends Component { return; } - const { name, value } = this.newAttribute; + const { name, value, isInheritable } = this.newAttribute; this.api.blockRedraw(); try { @@ -88,7 +89,7 @@ export default class TableColumnEditing extends Component { await renameColumn(this.parentNote.noteId, type as "label" | "relation", oldName, newName); } - attributes.setLabel(this.parentNote.noteId, name, value); + attributes.setLabel(this.parentNote.noteId, name, value, isInheritable); if (this.existingAttributeToEdit) { attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name); } From ebff644d243ff988ca0afb44946430815b1b841d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 13:31:46 +0300 Subject: [PATCH 13/17] fix(views/table): changing column inheritability not working --- .../view_widgets/table_view/col_editing.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts index cb3f90bc9..605e505b4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/col_editing.ts +++ b/apps/client/src/widgets/view_widgets/table_view/col_editing.ts @@ -82,17 +82,18 @@ export default class TableColumnEditing extends Component { const { name, value, isInheritable } = this.newAttribute; this.api.blockRedraw(); + const isRename = (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name); try { - if (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name) { - const oldName = this.existingAttributeToEdit.name.split(":")[1]; + if (isRename) { + const oldName = this.existingAttributeToEdit!.name.split(":")[1]; const [ type, newName ] = name.split(":"); await renameColumn(this.parentNote.noteId, type as "label" | "relation", oldName, newName); } - attributes.setLabel(this.parentNote.noteId, name, value, isInheritable); - if (this.existingAttributeToEdit) { + if (this.existingAttributeToEdit && (isRename || this.existingAttributeToEdit.isInheritable !== isInheritable)) { attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name); } + attributes.setLabel(this.parentNote.noteId, name, value, isInheritable); } finally { this.api.restoreRedraw(); } @@ -134,17 +135,17 @@ export default class TableColumnEditing extends Component { return this.parentNote.getLabel(attrName); } - getAttributeFromField(field: string) { + getAttributeFromField(field: string): Attribute | undefined { const fAttribute = this.getFAttributeFromField(field); if (fAttribute) { return { name: fAttribute.name, value: fAttribute.value, - type: fAttribute.type + type: fAttribute.type, + isInheritable: fAttribute.isInheritable }; } return undefined; } } - From 9dd0eb7b9bc1d97dc773d3dcd1b58ec24ee82624 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 14:02:19 +0300 Subject: [PATCH 14/17] fix(views/table): not reacting to external attribute changes --- 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 f59a37fb0..0dd47b27a 100644 --- a/apps/client/src/widgets/view_widgets/table_view/index.ts +++ b/apps/client/src/widgets/view_widgets/table_view/index.ts @@ -219,8 +219,8 @@ export default class TableView extends ViewMode { } if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? "")) - || loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId) - || loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!)))) { + || loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) + || loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!))) { return await this.#manageRowsUpdate(); } From ceb08593d80c8ad873f065bf755bc2a19bd7c671 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 14:04:25 +0300 Subject: [PATCH 15/17] chore(views/table): use translations for new label/relation --- apps/client/src/translations/en/translation.json | 4 +++- .../src/widgets/view_widgets/table_view/context_menu.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 281554d66..35e835e30 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1958,7 +1958,9 @@ "add-column-to-the-right": "Add column to the right", "edit-column": "Edit column", "delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.", - "delete-column": "Delete column" + "delete-column": "Delete column", + "new-column-label": "Label", + "new-column-relation": "Relation" }, "book_properties_config": { "hide-weekends": "Hide weekends", diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index efb28d5b9..e4de2198b 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -249,7 +249,7 @@ function buildColumnItems(tabulator: Tabulator) { function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, direction: "before" | "after"): MenuItem[] { return [ { - title: "Label", + title: t("table_view.new-column-label"), handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, @@ -259,7 +259,7 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, dir } }, { - title: "Relation", + title: t("table_view.new-column-relation"), handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, From cb8a5cbb6241924f642e1a133fbaca68074a5520 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 14:06:00 +0300 Subject: [PATCH 16/17] chore(views/table): add icons to add new column/row context menu --- apps/client/src/widgets/view_widgets/table_view/context_menu.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index e4de2198b..a63ab21ed 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -250,6 +250,7 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, dir return [ { title: t("table_view.new-column-label"), + uiIcon: "bx bx-hash", handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, @@ -260,6 +261,7 @@ function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, dir }, { title: t("table_view.new-column-relation"), + uiIcon: "bx bx-transfer", handler: () => { getParentComponent(e)?.triggerCommand("addNewTableColumn", { referenceColumn, From d487da0b2fbfd9fbb8cf5b70687734e3b212dbe4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 19 Jul 2025 14:17:48 +0300 Subject: [PATCH 17/17] feat(views/table): update new column in context menu to support relations also --- apps/client/src/menus/context_menu.ts | 5 +++++ .../src/widgets/view_widgets/table_view/context_menu.ts | 8 +++++--- .../Notes/Note List/Table View.html | 3 ++- .../Notes/Note List/Table View.md | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index aefe7bf30..4411db9dc 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -26,6 +26,11 @@ export interface MenuCommandItem { title: string; command?: T; type?: string; + /** + * The icon to display in the menu item. + * + * If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`. + */ uiIcon?: string; badges?: MenuItemBadge[]; templateNoteId?: string; diff --git a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts index a63ab21ed..53a6364d4 100644 --- a/apps/client/src/widgets/view_widgets/table_view/context_menu.ts +++ b/apps/client/src/widgets/view_widgets/table_view/context_menu.ts @@ -139,11 +139,13 @@ function showHeaderContextMenu(_e: Event, tabulator: Tabulator) { uiIcon: "bx bx-empty", items: buildColumnItems(tabulator) }, + { title: "----" }, { title: t("table_view.new-column"), - uiIcon: "bx bx-columns", - handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {}) + uiIcon: "bx bx-empty", + enabled: false }, + ...buildInsertSubmenu(e) ], selectMenuItemHandler() {}, x: e.pageX, @@ -246,7 +248,7 @@ function buildColumnItems(tabulator: Tabulator) { return items; } -function buildInsertSubmenu(e: MouseEvent, referenceColumn: ColumnComponent, direction: "before" | "after"): MenuItem[] { +function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem[] { return [ { title: t("table_view.new-column-label"), diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html index 92c9f3e7c..629b15342 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.html @@ -46,7 +46,8 @@
  • Press Add new column at the bottom of the table.
  • Right click on an existing column and select Add column to the left/right.
  • Right click on the empty space of the column header and select New column.
  • + data-list-item-id="e681ba5bf3901016423216783a17f13f8">Right click on the empty space of the column header and select Label or Relation in + the New column section.

    Adding new rows

    Each row is actually a note that is a child of the Collection note.

    diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md index d2a59b3ed..9e16684b6 100644 --- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md +++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table View.md @@ -35,7 +35,7 @@ To create a new column, either: * Press _Add new column_ at the bottom of the table. * Right click on an existing column and select Add column to the left/right. -* Right click on the empty space of the column header and select _New column_. +* Right click on the empty space of the column header and select _Label_ or _Relation_ in the _New column_ section. ### Adding new rows