2025-06-27 17:44:29 +03:00
|
|
|
import froca from "../../../services/froca.js";
|
|
|
|
|
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
|
2025-06-27 22:43:29 +03:00
|
|
|
import attributes, { setLabel } from "../../../services/attributes.js";
|
2025-06-28 12:24:40 +03:00
|
|
|
import getPromotedAttributeInformation, { buildColumnDefinitions, buildData, buildRowDefinitions, TableData } from "./data.js";
|
2025-06-27 17:44:29 +03:00
|
|
|
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";
|
2025-06-27 22:50:27 +03:00
|
|
|
import type { CommandListenerData, EventData } from "../../../components/app_context.js";
|
2025-06-27 22:43:29 +03:00
|
|
|
import type { Attribute } from "../../../services/attribute_parser.js";
|
2025-06-28 00:07:14 +03:00
|
|
|
import note_create from "../../../services/note_create.js";
|
2025-06-28 12:00:50 +03:00
|
|
|
import {Tabulator} from 'tabulator-tables';
|
2025-06-28 12:51:19 +03:00
|
|
|
import "tabulator-tables/dist/css/tabulator_bootstrap5.min.css";
|
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>
|
|
|
|
|
|
2025-06-27 22:19:09 +03:00
|
|
|
<div class="header">
|
|
|
|
|
<button data-trigger-command="addNoteListItem">Add new column</button>
|
2025-06-28 00:07:14 +03:00
|
|
|
<button data-trigger-command="addNewRow">Add new row</button>
|
2025-06-27 22:19:09 +03:00
|
|
|
</div>
|
|
|
|
|
|
2025-06-27 17:22:47 +03:00
|
|
|
<div class="table-view-container"></div>
|
2025-06-25 10:31:41 +03:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-06-27 17:40:56 +03:00
|
|
|
export interface StateInfo {
|
|
|
|
|
gridState: GridState;
|
|
|
|
|
}
|
|
|
|
|
|
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>;
|
2025-06-25 11:23:34 +03:00
|
|
|
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;
|
2025-06-27 22:43:29 +03:00
|
|
|
private newAttribute?: Attribute;
|
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");
|
2025-06-25 11:23:34 +03:00
|
|
|
this.args = args;
|
2025-06-27 17:58:25 +03:00
|
|
|
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
|
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() {
|
2025-06-27 17:39:57 +03:00
|
|
|
this.$container.empty();
|
|
|
|
|
this.renderTable(this.$container[0]);
|
|
|
|
|
return this.$root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async renderTable(el: HTMLElement) {
|
|
|
|
|
const viewStorage = await this.viewStorage.restore();
|
|
|
|
|
const initialState = viewStorage?.gridState;
|
|
|
|
|
|
2025-06-28 12:24:40 +03:00
|
|
|
this.api = new Tabulator(el, {
|
|
|
|
|
});
|
|
|
|
|
this.loadData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadData() {
|
|
|
|
|
if (!this.api) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 23:01:15 +03:00
|
|
|
const notes = await froca.getNotes(this.args.noteIds);
|
|
|
|
|
const info = getPromotedAttributeInformation(this.parentNote);
|
|
|
|
|
|
2025-06-28 12:24:40 +03:00
|
|
|
this.api.setColumns(buildColumnDefinitions(info));
|
|
|
|
|
this.api.setData(buildRowDefinitions(this.parentNote, notes, info));
|
2025-06-27 17:58:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onSave() {
|
|
|
|
|
if (!this.api) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.viewStorage.store({
|
|
|
|
|
gridState: this.api.getState()
|
2025-06-27 17:39:57 +03:00
|
|
|
});
|
2025-06-25 10:31:41 +03:00
|
|
|
}
|
|
|
|
|
|
2025-06-27 20:30:36 +03:00
|
|
|
private setupEditing(): GridOptions<TableData> {
|
|
|
|
|
return {
|
|
|
|
|
onCellValueChanged(event) {
|
|
|
|
|
if (event.type !== "cellValueChanged") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const noteId = event.data.noteId;
|
|
|
|
|
const name = event.colDef.field;
|
|
|
|
|
if (!name) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { newValue } = event;
|
|
|
|
|
if (name === "title") {
|
|
|
|
|
// TODO: Deduplicate with note_title.
|
|
|
|
|
server.put(`notes/${noteId}/title`, { title: newValue });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (name.startsWith("labels.")) {
|
|
|
|
|
const labelName = name.split(".", 2)[1];
|
|
|
|
|
setLabel(noteId, labelName, newValue);
|
|
|
|
|
}
|
2025-06-27 17:39:57 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-27 19:53:40 +03:00
|
|
|
|
2025-06-27 20:30:36 +03:00
|
|
|
private setupDragging() {
|
|
|
|
|
if (this.parentNote.hasLabel("sorted")) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2025-06-27 19:53:40 +03:00
|
|
|
|
2025-06-27 20:30:36 +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
|
|
|
}
|
2025-06-27 20:30:36 +03:00
|
|
|
};
|
|
|
|
|
return config;
|
|
|
|
|
}
|
2025-06-27 22:19:09 +03:00
|
|
|
|
|
|
|
|
async reloadAttributesCommand() {
|
|
|
|
|
console.log("Reload attributes");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) {
|
2025-06-27 22:43:29 +03:00
|
|
|
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);
|
2025-06-27 22:19:09 +03:00
|
|
|
}
|
2025-06-27 22:43:29 +03:00
|
|
|
|
2025-06-28 00:07:14 +03:00
|
|
|
addNewRowCommand() {
|
|
|
|
|
const parentNotePath = this.args.parentNotePath;
|
|
|
|
|
if (parentNotePath) {
|
|
|
|
|
note_create.createNote(parentNotePath, {
|
|
|
|
|
activate: false
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 23:40:00 +03:00
|
|
|
private getTheme(): Theme {
|
|
|
|
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
|
|
|
return themeQuartz.withPart(colorSchemeDark)
|
|
|
|
|
} else {
|
|
|
|
|
return themeQuartz;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:50:27 +03:00
|
|
|
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
|
|
|
|
|
// Refresh if promoted attributes get changed.
|
|
|
|
|
if (loadResults.getAttributeRows().find(attr =>
|
|
|
|
|
attr.type === "label" &&
|
|
|
|
|
attr.name?.startsWith("label:") &&
|
|
|
|
|
attributes.isAffecting(attr, this.parentNote))) {
|
2025-06-27 23:01:15 +03:00
|
|
|
const info = getPromotedAttributeInformation(this.parentNote);
|
|
|
|
|
const columnDefs = buildColumnDefinitions(info);
|
2025-06-28 16:39:24 +03:00
|
|
|
this.api?.setColumns(columnDefs)
|
2025-06-27 22:50:27 +03:00
|
|
|
}
|
|
|
|
|
|
2025-06-28 00:07:14 +03:00
|
|
|
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:50:27 +03:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 19:53:40 +03:00
|
|
|
}
|
2025-06-27 20:30:36 +03:00
|
|
|
|