Files
Trilium/apps/client/src/widgets/view_widgets/table_view/index.ts

255 lines
8.0 KiB
TypeScript
Raw Normal View History

import froca from "../../../services/froca.js";
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
import attributes, { setAttribute, setLabel } from "../../../services/attributes.js";
2025-06-28 12:24:40 +03:00
import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js";
import server from "../../../services/server.js";
2025-06-27 17:58:25 +03:00
import SpacedUpdate from "../../../services/spaced_update.js";
2025-06-27 19:53:40 +03:00
import branches from "../../../services/branches.js";
import type { CommandListenerData, EventData } from "../../../components/app_context.js";
import type { Attribute } from "../../../services/attribute_parser.js";
import note_create from "../../../services/note_create.js";
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule} from 'tabulator-tables';
import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css";
import { applyHeaderMenu } from "./header-menu.js";
2025-06-25 10:31:41 +03:00
const TPL = /*html*/`
<div class="table-view">
2025-06-25 10:40:04 +03:00
<style>
.table-view {
overflow: hidden;
position: relative;
height: 100%;
user-select: none;
padding: 10px;
}
2025-06-25 10:49:33 +03:00
.table-view-container {
height: 100%;
}
2025-06-27 17:18:52 +03:00
.search-result-widget-content .table-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
2025-06-25 10:40:04 +03:00
</style>
<div class="header">
<button data-trigger-command="addNoteListItem">Add new column</button>
<button data-trigger-command="addNewRow">Add new row</button>
</div>
<div class="table-view-container"></div>
2025-06-25 10:31:41 +03:00
</div>
`;
export interface StateInfo {
tableData: Record<string, object>;
}
2025-06-25 16:18:34 +03:00
export default class TableView extends ViewMode<StateInfo> {
2025-06-25 10:31:41 +03:00
private $root: JQuery<HTMLElement>;
2025-06-25 10:49:33 +03:00
private $container: JQuery<HTMLElement>;
private args: ViewModeArgs;
2025-06-27 17:58:25 +03:00
private spacedUpdate: SpacedUpdate;
2025-06-28 12:24:40 +03:00
private api?: Tabulator;
private newAttribute?: Attribute;
private persistentData: Record<string, object>;
2025-06-25 10:31:41 +03:00
constructor(args: ViewModeArgs) {
2025-06-25 16:18:34 +03:00
super(args, "table");
2025-06-25 10:31:41 +03:00
this.$root = $(TPL);
2025-06-25 10:49:33 +03:00
this.$container = this.$root.find(".table-view-container");
this.args = args;
2025-06-27 17:58:25 +03:00
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
this.persistentData = {};
2025-06-25 10:31:41 +03:00
args.$parent.append(this.$root);
}
2025-06-25 10:40:04 +03:00
get isFullHeight(): boolean {
return true;
}
2025-06-25 10:31:41 +03:00
async renderList() {
this.$container.empty();
this.renderTable(this.$container[0]);
return this.$root;
}
private async renderTable(el: HTMLElement) {
const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule];
2025-06-28 17:07:11 +03:00
for (const module of modules) {
Tabulator.registerModule(module);
}
this.initialize(el);
2025-06-28 12:24:40 +03:00
}
private async initialize(el: HTMLElement) {
const notes = await froca.getNotes(this.args.noteIds);
const info = getPromotedAttributeInformation(this.parentNote);
const columnDefs = buildColumnDefinitions(info);
applyHeaderMenu(columnDefs);
const viewStorage = await this.viewStorage.restore();
this.persistentData = viewStorage?.tableData || {};
this.api = new Tabulator(el, {
index: "noteId",
columns: columnDefs,
data: await buildRowDefinitions(this.parentNote, notes, info),
persistence: true,
movableColumns: true,
persistenceWriterFunc: (_id, type: string, data: object) => {
this.persistentData[type] = data;
this.spacedUpdate.scheduleUpdate();
},
persistenceReaderFunc: (_id, type: string) => this.persistentData[type],
});
this.setupEditing();
2025-06-27 17:58:25 +03:00
}
private onSave() {
this.viewStorage.store({
tableData: this.persistentData,
});
2025-06-25 10:31:41 +03:00
}
private setupEditing() {
this.api!.on("cellEdited", async (cell) => {
const noteId = cell.getRow().getData().noteId;
const field = cell.getField();
const newValue = cell.getValue();
if (field === "title") {
server.put(`notes/${noteId}/title`, { title: newValue });
return;
}
if (field.includes(".")) {
const [ type, name ] = field.split(".", 2);
if (type === "labels") {
setLabel(noteId, name, newValue);
} else if (type === "relations") {
const note = await froca.getNote(noteId);
if (note) {
setAttribute(note, "relation", name, newValue);
}
}
}
});
}
2025-06-27 19:53:40 +03:00
private setupDragging() {
if (this.parentNote.hasLabel("sorted")) {
return {};
}
2025-06-27 19:53:40 +03:00
const config: GridOptions<TableData> = {
rowDragEntireRow: true,
onRowDragEnd(e) {
const fromIndex = e.node.rowIndex;
const toIndex = e.overNode?.rowIndex;
if (fromIndex === null || toIndex === null || toIndex === undefined || fromIndex === toIndex) {
return;
}
const isBelow = (toIndex > fromIndex);
const fromBranchId = e.node.data?.branchId;
const toBranchId = e.overNode?.data?.branchId;
if (fromBranchId === undefined || toBranchId === undefined) {
return;
}
if (isBelow) {
branches.moveAfterBranch([ fromBranchId ], toBranchId);
} else {
branches.moveBeforeBranch([ fromBranchId ], toBranchId);
}
2025-06-27 19:53:40 +03:00
}
};
return config;
}
async reloadAttributesCommand() {
console.log("Reload attributes");
}
async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) {
this.newAttribute = attributes[0];
}
async saveAttributesCommand() {
if (!this.newAttribute) {
return;
}
const { name, value } = this.newAttribute;
attributes.addLabel(this.parentNote.noteId, name, value, true);
console.log("Save attributes", this.newAttribute);
}
addNewRowCommand() {
const parentNotePath = this.args.parentNotePath;
if (parentNotePath) {
note_create.createNote(parentNotePath, {
activate: false
});
}
}
private getTheme(): Theme {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return themeQuartz.withPart(colorSchemeDark)
} else {
return themeQuartz;
}
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
if (!this.api) {
return;
}
// Refresh if promoted attributes get changed.
if (loadResults.getAttributeRows().find(attr =>
attr.type === "label" &&
attr.name?.startsWith("label:") &&
attributes.isAffecting(attr, this.parentNote))) {
this.#manageColumnUpdate();
}
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) {
this.#manageRowsUpdate();
}
return false;
}
#manageColumnUpdate() {
2025-07-01 12:09:13 +03:00
if (!this.api) {
return;
}
const info = getPromotedAttributeInformation(this.parentNote);
const columnDefs = buildColumnDefinitions(info);
this.api.setColumns(columnDefs);
}
async #manageRowsUpdate() {
2025-07-01 12:09:13 +03:00
if (!this.api) {
return;
}
const notes = await froca.getNotes(this.args.noteIds);
const info = getPromotedAttributeInformation(this.parentNote);
this.api.setData(await buildRowDefinitions(this.parentNote, notes, info));
}
2025-06-27 19:53:40 +03:00
}