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/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/services/bulk_action.ts b/apps/client/src/services/bulk_action.ts
index 66922ef62..57b880c36 100644
--- a/apps/client/src/services/bulk_action.ts
+++ b/apps/client/src/services/bulk_action.ts
@@ -15,6 +15,8 @@ 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";
+import { BulkAction } from "@triliumnext/commons";
const ACTION_GROUPS = [
{
@@ -89,6 +91,17 @@ function parseActions(note: FNote) {
.filter((action) => !!action);
}
+export async function executeBulkActions(parentNoteId: string, actions: BulkAction[]) {
+ 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/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/bulk_actions.ts b/apps/client/src/widgets/view_widgets/table_view/bulk_actions.ts
index 48262b582..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,53 +1,31 @@
-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";
+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: {}) {
- 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
- });
-
- await ws.waitForMaxKnownEntityChangeId();
- toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
-}
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..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
@@ -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,20 +79,21 @@ export default class TableColumnEditing extends Component {
return;
}
- const { name, type, value } = this.newAttribute;
+ 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];
- const newName = name.split(":")[1];
- await renameColumn(this.parentNote.noteId, type, oldName, newName);
+ 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);
- 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();
}
@@ -133,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;
}
}
-
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..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,23 +248,27 @@ 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: "Label",
+ title: t("table_view.new-column-label"),
+ uiIcon: "bx bx-hash",
handler: () => {
getParentComponent(e)?.triggerCommand("addNewTableColumn", {
referenceColumn,
- type: "label"
+ type: "label",
+ direction
});
}
},
{
- title: "Relation",
+ title: t("table_view.new-column-relation"),
+ uiIcon: "bx bx-transfer",
handler: () => {
getParentComponent(e)?.triggerCommand("addNewTableColumn", {
referenceColumn,
- type: "relation"
+ type: "relation",
+ direction
});
}
}
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();
}
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..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
@@ -5,79 +5,177 @@
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 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.
-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
+ click outside the cell.
+
+
+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:
-
- - As mentioned previously, the columns of the table are defined as
- Promoted Attributes.
-
- - But only the promoted attributes that are defined at the level of the
- Collection note are actually taken into consideration.
- - There are plans to recursively look for columns across the sub-hierarchy.
-
+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 +184,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/apps/server/src/routes/api/bulk_action.ts b/apps/server/src/routes/api/bulk_action.ts
index 9a3a9ce1c..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.executeActions(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/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 1c57c6cd5..531bd9976 100644
--- a/apps/server/src/services/bulk_actions.ts
+++ b/apps/server/src/services/bulk_actions.ts
@@ -5,25 +5,15 @@ import branchService from "./branches.js";
import { randomString } from "./utils.js";
import eraseService from "./erase.js";
import type BNote from "../becca/entities/bnote.js";
+import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons";
-interface Action {
- labelName: string;
- labelValue: string;
- oldLabelName: string;
- newLabelName: string;
+type ActionHandler = (action: T, note: BNote) => void;
- relationName: string;
- oldRelationName: string;
- newRelationName: string;
+type ActionHandlerMap = {
+ [K in keyof ActionHandlers]: ActionHandler>;
+};
- targetNoteId: string;
- targetParentNoteId: string;
- newTitle: string;
- script: string;
-}
-type ActionHandler = (action: Action, note: BNote) => void;
-
-const ACTION_HANDLERS: Record = {
+const ACTION_HANDLERS: ActionHandlerMap = {
addLabel: (action, note) => {
note.addLabel(action.labelName, action.labelValue);
},
@@ -125,7 +115,7 @@ const ACTION_HANDLERS: Record = {
note.save();
}
-};
+} as const;
function getActions(note: BNote) {
return note
@@ -145,15 +135,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) {
@@ -164,7 +162,8 @@ function executeActions(note: BNote, searchResultNoteIds: 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] as (a: typeof action, n: BNote) => void;
+ handler(action, resultNote);
} catch (e: any) {
log.error(`ExecuteScript search action failed with ${e.message}`);
}
@@ -173,5 +172,6 @@ function executeActions(note: BNote, searchResultNoteIds: string[] | Set
}
export default {
- executeActions
+ executeActions,
+ executeActionsFromNote
};
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..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
@@ -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 _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.
-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 click outside the cell.
+
+### 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
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..d80c18974
--- /dev/null
+++ b/packages/commons/src/lib/bulk_actions.ts
@@ -0,0 +1,49 @@
+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 = {
+ [K in keyof ActionHandlers]: { name: K; } & ActionHandlers[K];
+}[keyof ActionHandlers];