Merge branch 'main' into feat/add-ocr-capabilities

This commit is contained in:
Jon Fuller 2025-07-15 10:10:47 -07:00 committed by GitHub
commit 02980834ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 675 additions and 334 deletions

View File

@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js";
import type { CreateNoteOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@ -276,6 +278,17 @@ export type CommandMappings = {
geoMapCreateChildNote: CommandData;
// Table view
addNewRow: CommandData & {
customOpts: CreateNoteOpts;
parentNotePath?: string;
};
addNewTableColumn: CommandData & {
columnToEdit?: ColumnComponent;
referenceColumn?: ColumnComponent;
direction?: "before" | "after";
};
buildTouchBar: CommandData & {
TouchBar: typeof TouchBar;
buildIcon(name: string): NativeImage;

View File

@ -256,6 +256,15 @@ class FNote {
return this.children;
}
async getSubtreeNoteIds() {
let noteIds: (string | string[])[] = [];
for (const child of await this.getChildNotes()) {
noteIds.push(child.noteId);
noteIds.push(await child.getSubtreeNoteIds());
}
return noteIds.flat();
}
async getChildNotes() {
return await this.froca.getNotes(this.children);
}

View File

@ -6,33 +6,18 @@ import TableView from "../widgets/view_widgets/table_view/index.js";
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
import type ViewMode from "../widgets/view_widgets/view_mode.js";
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap";
export default class NoteListRenderer {
private viewType: ViewTypeOptions;
public viewMode: ViewMode<any> | null;
private args: ArgsWithoutNoteId;
public viewMode?: ViewMode<any>;
constructor(args: ViewModeArgs) {
constructor(args: ArgsWithoutNoteId) {
this.args = args;
this.viewType = this.#getViewType(args.parentNote);
switch (this.viewType) {
case "list":
case "grid":
this.viewMode = new ListOrGridView(this.viewType, args);
break;
case "calendar":
this.viewMode = new CalendarView(args);
break;
case "table":
this.viewMode = new TableView(args);
break;
case "geoMap":
this.viewMode = new GeoView(args);
break;
default:
this.viewMode = null;
}
}
#getViewType(parentNote: FNote): ViewTypeOptions {
@ -47,15 +32,36 @@ export default class NoteListRenderer {
}
get isFullHeight() {
return this.viewMode?.isFullHeight;
switch (this.viewType) {
case "list":
case "grid":
return false;
default:
return true;
}
}
async renderList() {
if (!this.viewMode) {
return null;
const args = this.args;
const viewMode = this.#buildViewMode(args);
this.viewMode = viewMode;
await viewMode.beforeRender();
return await viewMode.renderList();
}
return await this.viewMode.renderList();
#buildViewMode(args: ViewModeArgs) {
switch (this.viewType) {
case "calendar":
return new CalendarView(args);
case "table":
return new TableView(args);
case "geoMap":
return new GeoView(args);
case "list":
case "grid":
default:
return new ListOrGridView(this.viewType, args);
}
}
}

View File

@ -1967,7 +1967,11 @@
"hide-column": "Hide column \"{{title}}\"",
"show-hide-columns": "Show/hide columns",
"row-insert-above": "Insert row above",
"row-insert-below": "Insert row below"
"row-insert-below": "Insert row below",
"row-insert-child": "Insert child note",
"add-column-to-the-left": "Add column to the left",
"add-column-to-the-right": "Add column to the right",
"edit-column": "Edit column"
},
"book_properties_config": {
"hide-weekends": "Hide weekends",

View File

@ -295,6 +295,7 @@ interface AttributeDetailOpts {
x: number;
y: number;
focus?: "name";
parent?: HTMLElement;
}
interface SearchRelatedResponse {
@ -560,18 +561,21 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.toggleInt(true);
const offset = this.parent?.$widget.offset() || { top: 0, left: 0 };
const offset = this.parent?.$widget?.offset() || { top: 0, left: 0 };
const detPosition = this.getDetailPosition(x, offset);
const outerHeight = this.$widget.outerHeight();
const height = $(window).height();
if (detPosition && outerHeight && height) {
if (!detPosition || !outerHeight || !height) {
console.warn("Can't position popup, is it attached?");
return;
}
this.$widget
.css("left", detPosition.left)
.css("right", detPosition.right)
.css("top", y - offset.top + 70)
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
}
if (focus === "name") {
this.$inputName.trigger("focus").trigger("select");

View File

@ -426,7 +426,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
curNode = curNode.previousSibling;
if ((curNode as ModelElement).name === "reference") {
clickIndex += (curNode.getAttribute("notePath") as string).length + 1;
clickIndex += (curNode.getAttribute("href") as string).length + 1;
} else if ("data" in curNode) {
clickIndex += (curNode.data as string).length;
}

View File

@ -3,8 +3,6 @@ import NoteListRenderer from "../services/note_list_renderer.js";
import type FNote from "../entities/fnote.js";
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
import type ViewMode from "./view_widgets/view_mode.js";
import AttributeDetailWidget from "./attribute_widgets/attribute_detail.js";
import { Attribute } from "../services/attribute_parser.js";
const TPL = /*html*/`
<div class="note-list-widget">
@ -39,7 +37,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
private noteIdRefreshed?: string;
private shownNoteId?: string | null;
private viewMode?: ViewMode<any> | null;
private attributeDetailWidget: AttributeDetailWidget;
private displayOnlyCollections: boolean;
/**
@ -47,9 +44,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
*/
constructor(displayOnlyCollections: boolean) {
super();
this.attributeDetailWidget = new AttributeDetailWidget()
.contentSized()
.setParent(this);
this.displayOnlyCollections = displayOnlyCollections;
}
@ -72,7 +67,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
this.$widget = $(TPL);
this.contentSized();
this.$content = this.$widget.find(".note-list-widget-content");
this.$widget.append(this.attributeDetailWidget.render());
const observer = new IntersectionObserver(
(entries) => {
@ -91,23 +85,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
setTimeout(() => observer.observe(this.$widget[0]), 10);
}
addNoteListItemEvent() {
const attr: Attribute = {
type: "label",
name: "label:myLabel",
value: "promoted,single,text"
};
this.attributeDetailWidget!.showAttributeDetail({
attribute: attr,
allAttributes: [ attr ],
isOwned: true,
x: 100,
y: 200,
focus: "name"
});
}
checkRenderStatus() {
// console.log("this.isIntersecting", this.isIntersecting);
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
@ -123,8 +100,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
const noteListRenderer = new NoteListRenderer({
$parent: this.$content,
parentNote: note,
parentNotePath: this.notePath,
noteIds: note.getChildNoteIds()
parentNotePath: this.notePath
});
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
await noteListRenderer.renderList();
@ -169,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
this.refresh();
this.checkRenderStatus();
}
// Inform the view mode of changes and refresh if needed.
if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) {
this.refresh();
this.checkRenderStatus();
}
}
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {

View File

@ -68,7 +68,6 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
const noteListRenderer = new NoteListRenderer({
$parent: this.$content,
parentNote: note,
noteIds: note.getChildNoteIds(),
showNotePath: true
});
await noteListRenderer.renderList();

View File

@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) {
const templateNoteIds = new Set(templateCache.keys());
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
await froca.getNotes(affectedNoteIds);
await froca.getNotes(affectedNoteIds, true);
let fullReloadNeeded = false;
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {

View File

@ -265,8 +265,13 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
focus() {
const editor = this.watchdog.editor;
if (editor) {
editor.editing.view.focus();
} else {
this.$editor.trigger("focus");
}
}
scrollToEnd() {
this.watchdog?.editor?.model.change((writer) => {

View File

@ -71,6 +71,15 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
}
}
activeNoteChangedEvent() {
if (!this.isActiveNoteContext()) {
return;
}
// Restore focus to the editor when switching tabs.
this.focus();
}
/**
* {@inheritdoc}
*

View File

@ -113,7 +113,6 @@ export default class CalendarView extends ViewMode<{}> {
private $root: JQuery<HTMLElement>;
private $calendarContainer: JQuery<HTMLElement>;
private noteIds: string[];
private calendar?: Calendar;
private isCalendarRoot: boolean;
private lastView?: string;
@ -124,15 +123,10 @@ export default class CalendarView extends ViewMode<{}> {
this.$root = $(TPL);
this.$calendarContainer = this.$root.find(".calendar-container");
this.noteIds = args.noteIds;
this.isCalendarRoot = false;
args.$parent.append(this.$root);
}
get isFullHeight(): boolean {
return true;
}
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
const isEditable = !this.isCalendarRoot;
@ -396,7 +390,7 @@ export default class CalendarView extends ViewMode<{}> {
}
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
// Refresh note IDs if they got changed.
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
this.noteIds = this.parentNote.getChildNoteIds();
@ -438,7 +432,7 @@ export default class CalendarView extends ViewMode<{}> {
events.push(await CalendarView.buildEvent(dateNote, { startDate }));
if (dateNote.hasChildren()) {
const childNoteIds = dateNote.getChildNoteIds();
const childNoteIds = await dateNote.getSubtreeNoteIds();
for (const childNoteId of childNoteIds) {
childNoteToDateMapping[childNoteId] = startDate;
}
@ -464,13 +458,6 @@ export default class CalendarView extends ViewMode<{}> {
for (const note of notes) {
const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate");
if (note.hasChildren()) {
const childrenEventData = await this.buildEvents(note.getChildNoteIds());
if (childrenEventData.length > 0) {
events.push(childrenEventData);
}
}
if (!startDate) {
continue;
}

View File

@ -243,10 +243,6 @@ export default class GeoView extends ViewMode<MapData> {
}
}
get isFullHeight(): boolean {
return true;
}
#changeState(newState: State) {
this._state = newState;
this.$container.toggleClass("placing-note", newState === State.NewNote);
@ -255,7 +251,7 @@ export default class GeoView extends ViewMode<MapData> {
}
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
// If any of the children branches are altered.
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) {
this.#reloadMarkers();

View File

@ -161,7 +161,7 @@ const TPL = /*html*/`
class ListOrGridView extends ViewMode<{}> {
private $noteList: JQuery<HTMLElement>;
private noteIds: string[];
private filteredNoteIds!: string[];
private page?: number;
private pageSize?: number;
private showNotePath?: boolean;
@ -174,13 +174,6 @@ class ListOrGridView extends ViewMode<{}> {
super(args, viewType);
this.$noteList = $(TPL);
const includedNoteIds = this.getIncludedNoteIds();
this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
if (this.noteIds.length === 0) {
return;
}
args.$parent.append(this.$noteList);
@ -204,8 +197,14 @@ class ListOrGridView extends ViewMode<{}> {
return new Set(includedLinks.map((rel) => rel.value));
}
async beforeRender() {
super.beforeRender();
const includedNoteIds = this.getIncludedNoteIds();
this.filteredNoteIds = this.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
}
async renderList() {
if (this.noteIds.length === 0 || !this.page || !this.pageSize) {
if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) {
this.$noteList.hide();
return;
}
@ -226,7 +225,7 @@ class ListOrGridView extends ViewMode<{}> {
const startIdx = (this.page - 1) * this.pageSize;
const endIdx = startIdx + this.pageSize;
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
const pageNoteIds = this.filteredNoteIds.slice(startIdx, Math.min(endIdx, this.filteredNoteIds.length));
const pageNotes = await froca.getNotes(pageNoteIds);
for (const note of pageNotes) {
@ -246,7 +245,7 @@ class ListOrGridView extends ViewMode<{}> {
return;
}
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize);
$pager.toggle(pageCount > 1);
@ -257,7 +256,7 @@ class ListOrGridView extends ViewMode<{}> {
lastPrinted = true;
const startIndex = (i - 1) * this.pageSize + 1;
const endIndex = Math.min(this.noteIds.length, i * this.pageSize);
const endIndex = Math.min(this.filteredNoteIds.length, i * this.pageSize);
$pager.append(
i === this.page
@ -279,7 +278,7 @@ class ListOrGridView extends ViewMode<{}> {
}
// no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all
$pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`);
$pager.append(`<span class="note-list-pager-total-count">(${this.filteredNoteIds.length} notes)</span>`);
}
async renderNote(note: FNote, expand: boolean = false) {

View File

@ -0,0 +1,104 @@
import { Tabulator } from "tabulator-tables";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import { Attribute } from "../../../services/attribute_parser";
import Component from "../../../components/component";
import { CommandListenerData, EventData } from "../../../components/app_context";
import attributes from "../../../services/attributes";
import FNote from "../../../entities/fnote";
export default class TableColumnEditing extends Component {
private attributeDetailWidget: AttributeDetailWidget;
private newAttributePosition?: number;
private api: Tabulator;
private newAttribute?: Attribute;
private parentNote: FNote;
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, api: Tabulator) {
super();
const parentComponent = glob.getComponentByEl($parent[0]);
this.attributeDetailWidget = new AttributeDetailWidget()
.contentSized()
.setParent(parentComponent);
$parent.append(this.attributeDetailWidget.render());
this.api = api;
this.parentNote = parentNote;
}
addNewTableColumnCommand({ referenceColumn, columnToEdit, direction }: EventData<"addNewTableColumn">) {
let attr: Attribute | undefined;
if (columnToEdit) {
attr = this.getAttributeFromField(columnToEdit.getField());
console.log("Built ", attr);
}
if (!attr) {
attr = {
type: "label",
name: "label:myLabel",
value: "promoted,single,text"
};
}
if (referenceColumn && this.api) {
this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn);
if (direction === "after") {
this.newAttributePosition++;
}
} else {
this.newAttributePosition = undefined;
}
this.attributeDetailWidget!.showAttributeDetail({
attribute: attr,
allAttributes: [ attr ],
isOwned: true,
x: 0,
y: 150,
focus: "name"
});
}
async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) {
this.newAttribute = attributes[0];
}
async saveAttributesCommand() {
if (!this.newAttribute) {
return;
}
const { name, value } = this.newAttribute;
attributes.setLabel(this.parentNote.noteId, name, value);
}
getNewAttributePosition() {
return this.newAttributePosition;
}
resetNewAttributePosition() {
this.newAttributePosition = 0;
}
getFAttributeFromField(field: string) {
const [ type, name ] = field.split(".", 2);
const attrName = `${type.replace("s", "")}:${name}`;
return this.parentNote.getLabel(attrName);
}
getAttributeFromField(field: string) {
const fAttribute = this.getFAttributeFromField(field);
if (fAttribute) {
return {
name: fAttribute.name,
value: fAttribute.value,
type: fAttribute.type
};
}
return undefined;
}
}

View File

@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { restoreExistingData } from "./columns";
import type { ColumnDefinition } from "tabulator-tables";
describe("restoreExistingData", () => {
it("maintains important columns properties", () => {
const newDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", editor: "input" },
{ field: "noteId", title: "Note ID", formatter: "color", visible: false }
];
const oldDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", width: 300, visible: true },
{ field: "noteId", title: "Note ID", width: 200, visible: true }
];
const restored = restoreExistingData(newDefs, oldDefs);
expect(restored[0].editor).toBe("input");
expect(restored[1].formatter).toBe("color");
});
it("should restore existing column data", () => {
const newDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", editor: "input" },
{ field: "noteId", title: "Note ID", visible: false }
];
const oldDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", width: 300, visible: true },
{ field: "noteId", title: "Note ID", width: 200, visible: true }
];
const restored = restoreExistingData(newDefs, oldDefs);
expect(restored[0].width).toBe(300);
expect(restored[1].width).toBe(200);
});
it("restores order of columns", () => {
const newDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", editor: "input" },
{ field: "noteId", title: "Note ID", visible: false }
];
const oldDefs: ColumnDefinition[] = [
{ field: "noteId", title: "Note ID", width: 200, visible: true },
{ field: "title", title: "Title", width: 300, visible: true }
];
const restored = restoreExistingData(newDefs, oldDefs);
expect(restored[0].field).toBe("noteId");
expect(restored[1].field).toBe("title");
});
it("inserts new columns at given position", () => {
const newDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", editor: "input" },
{ field: "noteId", title: "Note ID", visible: false },
{ field: "newColumn", title: "New Column", editor: "input" }
];
const oldDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", width: 300, visible: true },
{ field: "noteId", title: "Note ID", width: 200, visible: true }
];
const restored = restoreExistingData(newDefs, oldDefs, 0);
expect(restored.length).toBe(3);
expect(restored[0].field).toBe("newColumn");
expect(restored[1].field).toBe("title");
expect(restored[2].field).toBe("noteId");
});
it("inserts new columns at the end if no position is specified", () => {
const newDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", editor: "input" },
{ field: "noteId", title: "Note ID", visible: false },
{ field: "newColumn", title: "New Column", editor: "input" }
];
const oldDefs: ColumnDefinition[] = [
{ field: "title", title: "Title", width: 300, visible: true },
{ field: "noteId", title: "Note ID", width: 200, visible: true }
];
const restored = restoreExistingData(newDefs, oldDefs);
expect(restored.length).toBe(3);
expect(restored[0].field).toBe("title");
expect(restored[1].field).toBe("noteId");
expect(restored[2].field).toBe("newColumn");
});
});

View File

@ -1,12 +1,11 @@
import { RelationEditor } from "./relation_editor.js";
import { NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
import { applyHeaderMenu } from "./header-menu.js";
import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
import type { ColumnDefinition } from "tabulator-tables";
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
type ColumnType = LabelType | "relation";
export interface PromotedAttributeInformation {
export interface AttributeDefinitionInformation {
name: string;
title?: string;
type?: ColumnType;
@ -42,20 +41,21 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
}
};
export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) {
const columnDefs: ColumnDefinition[] = [
export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[], position?: number) {
let columnDefs: ColumnDefinition[] = [
{
title: "#",
headerSort: false,
hozAlign: "center",
resizable: false,
frozen: true,
rowHandle: true,
formatter: RowNumberFormatter
rowHandle: movableRows,
formatter: RowNumberFormatter(movableRows)
},
{
field: "noteId",
title: "Note ID",
formatter: MonospaceFormatter,
visible: false
},
{
@ -86,27 +86,41 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[], exi
seenFields.add(field);
}
applyHeaderMenu(columnDefs);
if (existingColumnData) {
restoreExistingData(columnDefs, existingColumnData);
columnDefs = restoreExistingData(columnDefs, existingColumnData, position);
}
return columnDefs;
}
function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) {
const byField = new Map<string, ColumnDefinition>;
for (const def of oldDefs) {
byField.set(def.field ?? "", def);
}
export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) {
// 1. Keep existing columns, but restore their properties like width, visibility and order.
const newItemsByField = new Map<string, ColumnDefinition>(
newDefs.map(def => [def.field!, def])
);
const existingColumns = oldDefs
.map(item => {
return {
...newItemsByField.get(item.field!),
width: item.width,
visible: item.visible,
};
}) as ColumnDefinition[];
for (const newDef of newDefs) {
const oldDef = byField.get(newDef.field ?? "");
if (!oldDef) {
continue;
}
// 2. Determine new columns.
const existingFields = new Set(existingColumns.map(item => item.field));
const newColumns = newDefs
.filter(item => !existingFields.has(item.field!));
newDef.width = oldDef.width;
newDef.visible = oldDef.visible;
}
// Clamp position to a valid range
const insertPos = position !== undefined
? Math.min(Math.max(position, 0), existingColumns.length)
: existingColumns.length;
// 3. Insert new columns at the specified position
return [
...existingColumns.slice(0, insertPos),
...newColumns,
...existingColumns.slice(insertPos)
];
}

View File

@ -5,10 +5,19 @@ import branches from "../../../services/branches.js";
import { t } from "../../../services/i18n.js";
import link_context_menu from "../../../menus/link_context_menu.js";
import type FNote from "../../../entities/fnote.js";
import froca from "../../../services/froca.js";
import type Component from "../../../components/component.js";
export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) {
tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote));
tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator));
tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, tabulator));
// Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't.
if (tabulator.options.dataTree) {
const dismissContextMenu = () => contextMenu.hide();
tabulator.on("dataTreeRowExpanded", dismissContextMenu);
tabulator.on("dataTreeRowCollapsed", dismissContextMenu);
}
}
function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: Tabulator) {
@ -69,6 +78,29 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator:
uiIcon: "bx bx-empty",
items: buildColumnItems()
},
{ title: "----" },
{
title: t("table_view.add-column-to-the-left"),
uiIcon: "bx bx-horizontal-left",
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
referenceColumn: column
})
},
{
title: t("table_view.edit-column"),
uiIcon: "bx bx-edit",
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
columnToEdit: column
})
},
{
title: t("table_view.add-column-to-the-right"),
uiIcon: "bx bx-horizontal-right",
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
referenceColumn: column,
direction: "after"
})
}
],
selectMenuItemHandler() {},
x: e.pageX,
@ -79,11 +111,11 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator:
function buildColumnItems() {
const items: MenuItem<unknown>[] = [];
for (const column of tabulator.getColumns()) {
const { title, visible, field } = column.getDefinition();
const { title, field } = column.getDefinition();
items.push({
title,
checked: visible,
checked: column.isVisible(),
uiIcon: "bx bx-empty",
enabled: !!field,
handler: () => column.toggle()
@ -94,9 +126,19 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator:
}
}
export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote) {
export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) {
const e = _e as MouseEvent;
const rowData = row.getData() as TableData;
let parentNoteId: string = parentNote.noteId;
if (tabulator.options.dataTree) {
const parentRow = row.getTreeParent();
if (parentRow) {
parentNoteId = parentRow.getData().noteId as string;
}
}
contextMenu.show({
items: [
...link_context_menu.getItems(),
@ -104,36 +146,39 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
{
title: t("table_view.row-insert-above"),
uiIcon: "bx bx-list-plus",
handler: () => {
const target = e.target;
if (!target) {
return;
}
const component = $(target).closest(".component").prop("component");
component.triggerCommand("addNewRow", {
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId,
customOpts: {
target: "before",
targetBranchId: rowData.branchId,
}
})
},
{
title: t("table_view.row-insert-child"),
uiIcon: "bx bx-empty",
handler: async () => {
const branchId = row.getData().branchId;
const note = await froca.getBranch(branchId)?.getNote();
getParentComponent(e)?.triggerCommand("addNewRow", {
parentNotePath: note?.noteId,
customOpts: {
target: "after",
targetBranchId: branchId,
}
});
}
},
{
title: t("table_view.row-insert-below"),
uiIcon: "bx bx-empty",
handler: () => {
const target = e.target;
if (!target) {
return;
}
const component = $(target).closest(".component").prop("component");
component.triggerCommand("addNewRow", {
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId,
customOpts: {
target: "after",
targetBranchId: rowData.branchId,
}
});
}
})
},
{ title: "----" },
{
@ -148,3 +193,13 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
});
e.preventDefault();
}
function getParentComponent(e: MouseEvent) {
if (!e.target) {
return;
}
return $(e.target)
.closest(".component")
.prop("component") as Component;
}

View File

@ -15,7 +15,7 @@ export default function buildFooter(parentNote: FNote) {
<span class="bx bx-plus"></span> ${t("table_view.new-row")}
</button>
<button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNoteListItem">
<button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNewTableColumn">
<span class="bx bx-columns"></span> ${t("table_view.new-column")}
</button>
`.trimStart();

View File

@ -36,8 +36,19 @@ export function NoteTitleFormatter(cell: CellComponent) {
return $noteRef[0].outerHTML;
}
export function RowNumberFormatter(cell: CellComponent) {
return `<span class="bx bx-dots-vertical-rounded"></span> ` + cell.getRow().getPosition(true);
export function RowNumberFormatter(draggableRows: boolean) {
return (cell: CellComponent) => {
let html = "";
if (draggableRows) {
html += `<span class="bx bx-dots-vertical-rounded"></span> `;
}
html += cell.getRow().getPosition(true);
return html;
};
}
export function MonospaceFormatter(cell: CellComponent) {
return `<code>${cell.getValue()}</code>`;
}
function buildNoteLink(noteId: string) {

View File

@ -1,53 +0,0 @@
import type { ColumnComponent, ColumnDefinition, MenuObject, Tabulator } from "tabulator-tables";
export function applyHeaderMenu(columns: ColumnDefinition[]) {
for (let column of columns) {
if (column.headerSort !== false) {
column.headerMenu = headerMenu;
}
}
}
function headerMenu(this: Tabulator) {
const menu: MenuObject<ColumnComponent>[] = [];
const columns = this.getColumns();
for (let column of columns) {
//create checkbox element using font awesome icons
let icon = document.createElement("i");
icon.classList.add("bx");
icon.classList.add(column.isVisible() ? "bx-check" : "bx-empty");
//build label
let label = document.createElement("span");
let title = document.createElement("span");
title.textContent = " " + column.getDefinition().title;
label.appendChild(icon);
label.appendChild(title);
//create menu item
menu.push({
label: label,
action: function (e) {
//prevent menu closing
e.stopPropagation();
//toggle current column visibility
column.toggle();
//change menu item icon
if (column.isVisible()) {
icon.classList.remove("bx-empty");
icon.classList.add("bx-check");
} else {
icon.classList.remove("bx-check");
icon.classList.add("bx-empty");
}
}
});
}
return menu;
};

View File

@ -1,19 +1,17 @@
import froca from "../../../services/froca.js";
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
import attributes, { setAttribute, setLabel } from "../../../services/attributes.js";
import server from "../../../services/server.js";
import attributes from "../../../services/attributes.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import type { CommandListenerData, EventData } from "../../../components/app_context.js";
import type { Attribute } from "../../../services/attribute_parser.js";
import note_create, { CreateNoteOpts } from "../../../services/note_create.js";
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MenuModule, MoveRowsModule, ColumnDefinition} from 'tabulator-tables';
import type { EventData } from "../../../components/app_context.js";
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables';
import "tabulator-tables/dist/css/tabulator.css";
import "../../../../src/stylesheets/table.css";
import { canReorderRows, configureReorderingRows } from "./dragging.js";
import buildFooter from "./footer.js";
import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js";
import { buildColumnDefinitions } from "./columns.js";
import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js";
import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js";
import { setupContextMenu } from "./context_menu.js";
import TableColumnEditing from "./col_editing.js";
import TableRowEditing from "./row_editing.js";
const TPL = /*html*/`
<div class="table-view">
@ -65,6 +63,26 @@ const TPL = /*html*/`
justify-content: left;
gap: 0.5em;
}
.tabulator button.tree-expand,
.tabulator button.tree-collapse {
display: inline-block;
appearance: none;
border: 0;
background: transparent;
width: 1.5em;
position: relative;
vertical-align: middle;
}
.tabulator button.tree-expand span,
.tabulator button.tree-collapse span {
position: absolute;
top: 0;
left: 0;
font-size: 1.5em;
transform: translateY(-50%);
}
</style>
<div class="table-view-container"></div>
@ -81,29 +99,22 @@ export default class TableView extends ViewMode<StateInfo> {
private $root: JQuery<HTMLElement>;
private $container: JQuery<HTMLElement>;
private args: ViewModeArgs;
private spacedUpdate: SpacedUpdate;
private api?: Tabulator;
private newAttribute?: Attribute;
private persistentData: StateInfo["tableData"];
/** If set to a note ID, whenever the rows will be updated, the title of the note will be automatically focused for editing. */
private noteIdToEdit?: string;
private colEditing?: TableColumnEditing;
private rowEditing?: TableRowEditing;
constructor(args: ViewModeArgs) {
super(args, "table");
this.$root = $(TPL);
this.$container = this.$root.find(".table-view-container");
this.args = args;
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
this.persistentData = {};
args.$parent.append(this.$root);
}
get isFullHeight(): boolean {
return true;
}
async renderList() {
this.$container.empty();
this.renderTable(this.$container[0]);
@ -111,29 +122,27 @@ export default class TableView extends ViewMode<StateInfo> {
}
private async renderTable(el: HTMLElement) {
const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, MenuModule];
const info = getAttributeDefinitionInformation(this.parentNote);
const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ];
for (const module of modules) {
Tabulator.registerModule(module);
}
this.initialize(el);
this.initialize(el, info);
}
private async initialize(el: HTMLElement) {
const notes = await froca.getNotes(this.args.noteIds);
const info = getPromotedAttributeInformation(this.parentNote);
private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) {
const viewStorage = await this.viewStorage.restore();
this.persistentData = viewStorage?.tableData || {};
const columnDefs = buildColumnDefinitions(info);
const movableRows = canReorderRows(this.parentNote);
this.api = new Tabulator(el, {
const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info);
const movableRows = canReorderRows(this.parentNote) && !hasChildren;
const columnDefs = buildColumnDefinitions(info, movableRows);
let opts: Options = {
layout: "fitDataFill",
index: "noteId",
index: "branchId",
columns: columnDefs,
data: await buildRowDefinitions(this.parentNote, notes, info),
data: rowData,
persistence: true,
movableColumns: true,
movableRows,
@ -143,10 +152,28 @@ export default class TableView extends ViewMode<StateInfo> {
this.spacedUpdate.scheduleUpdate();
},
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
});
};
if (hasChildren) {
opts = {
...opts,
dataTree: hasChildren,
dataTreeStartExpanded: true,
dataTreeElementColumn: "title",
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
}
}
this.api = new Tabulator(el, opts);
this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api);
this.rowEditing = new TableRowEditing(this.api, this.args.parentNotePath!);
if (movableRows) {
configureReorderingRows(this.api);
}
setupContextMenu(this.api, this.parentNote);
this.setupEditing();
}
private onSave() {
@ -155,86 +182,29 @@ export default class TableView extends ViewMode<StateInfo> {
});
}
private setupEditing() {
this.api!.on("cellEdited", async (cell) => {
const noteId = cell.getRow().getData().noteId;
const field = cell.getField();
let 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") {
if (typeof newValue === "boolean") {
newValue = newValue ? "true" : "false";
}
setLabel(noteId, name, newValue);
} else if (type === "relations") {
const note = await froca.getNote(noteId);
if (note) {
setAttribute(note, "relation", name, newValue);
}
}
}
});
}
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({ customOpts }: { customOpts: CreateNoteOpts }) {
const parentNotePath = this.args.parentNotePath;
if (parentNotePath) {
const opts: CreateNoteOpts = {
activate: false,
...customOpts
}
console.log("Create with ", opts);
note_create.createNote(parentNotePath, opts).then(({ note }) => {
if (!note) {
return;
}
this.noteIdToEdit = note.noteId;
})
}
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.api) {
return;
}
// Force a refresh if sorted is changed since we need to disable reordering.
if (loadResults.getAttributeRows().find(a => a.name === "sorted" && attributes.isAffecting(a, this.parentNote))) {
return true;
}
// Refresh if promoted attributes get changed.
if (loadResults.getAttributeRows().find(attr =>
attr.type === "label" &&
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
attributes.isAffecting(attr, this.parentNote))) {
console.log("Col update");
this.#manageColumnUpdate();
}
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)
|| loadResults.getNoteIds().some(noteId => this.args.noteIds.includes(noteId)
|| loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!)))) {
this.#manageRowsUpdate();
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!)))) {
return await this.#manageRowsUpdate();
}
return false;
@ -245,27 +215,44 @@ export default class TableView extends ViewMode<StateInfo> {
return;
}
const info = getPromotedAttributeInformation(this.parentNote);
const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns);
const info = getAttributeDefinitionInformation(this.parentNote);
const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.colEditing?.getNewAttributePosition());
this.api.setColumns(columnDefs);
this.colEditing?.resetNewAttributePosition();
}
addNewRowCommand(e) {
this.rowEditing?.addNewRowCommand(e);
}
addNewTableColumnCommand(e) {
this.colEditing?.addNewTableColumnCommand(e);
}
updateAttributeListCommand(e) {
this.colEditing?.updateAttributeListCommand(e);
}
saveAttributesCommand() {
this.colEditing?.saveAttributesCommand();
}
async #manageRowsUpdate() {
if (!this.api) {
return;
}
const notes = await froca.getNotes(this.args.noteIds);
const info = getPromotedAttributeInformation(this.parentNote);
this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info));
const info = getAttributeDefinitionInformation(this.parentNote);
const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info);
if (this.noteIdToEdit) {
const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit);
if (row) {
row.getCell("title").edit();
}
this.noteIdToEdit = undefined;
// Force a refresh if the data tree needs enabling/disabling.
if (this.api.options.dataTree !== hasSubtree) {
return true;
}
await this.api.replaceData(definitions);
return false;
}
}

View File

@ -0,0 +1,97 @@
import { RowComponent, Tabulator } from "tabulator-tables";
import Component from "../../../components/component.js";
import { setAttribute, setLabel } from "../../../services/attributes.js";
import server from "../../../services/server.js";
import froca from "../../../services/froca.js";
import note_create, { CreateNoteOpts } from "../../../services/note_create.js";
import { CommandListenerData } from "../../../components/app_context.js";
export default class TableRowEditing extends Component {
private parentNotePath: string;
private api: Tabulator;
constructor(api: Tabulator, parentNotePath: string) {
super();
this.api = api;
this.parentNotePath = parentNotePath;
api.on("cellEdited", async (cell) => {
const noteId = cell.getRow().getData().noteId;
const field = cell.getField();
let 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") {
if (typeof newValue === "boolean") {
newValue = newValue ? "true" : "false";
}
setLabel(noteId, name, newValue);
} else if (type === "relations") {
const note = await froca.getNote(noteId);
if (note) {
setAttribute(note, "relation", name, newValue);
}
}
}
});
}
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
const parentNotePath = customNotePath ?? this.parentNotePath;
if (parentNotePath) {
const opts: CreateNoteOpts = {
activate: false,
...customOpts
}
note_create.createNote(parentNotePath, opts).then(({ branch }) => {
if (branch) {
setTimeout(() => {
this.focusOnBranch(branch?.branchId);
});
}
})
}
}
focusOnBranch(branchId: string) {
if (!this.api) {
return;
}
const row = findRowDataById(this.api.getRows(), branchId);
if (!row) {
return;
}
// Expand the parent tree if any.
if (this.api.options.dataTree) {
const parent = row.getTreeParent();
if (parent) {
parent.treeExpand();
}
}
row.getCell("title").edit();
}
}
function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null {
for (let row of rows) {
const item = row.getIndex() as string;
if (item === branchId) {
return row;
}
let found = findRowDataById(row.getTreeChildren(), branchId);
if (found) return found;
}
return null;
}

View File

@ -1,6 +1,6 @@
import FNote from "../../../entities/fnote.js";
import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
import type { PromotedAttributeInformation } from "./columns.js";
import type { AttributeDefinitionInformation } from "./columns.js";
export type TableData = {
iconClass: string;
@ -9,10 +9,12 @@ export type TableData = {
labels: Record<string, boolean | string | null>;
relations: Record<string, boolean | string | null>;
branchId: string;
_children?: TableData[];
};
export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) {
export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[]) {
const definitions: TableData[] = [];
let hasSubtree = false;
for (const branch of parentNote.getChildBranches()) {
const note = await branch.getNote();
if (!note) {
@ -28,30 +30,43 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf
labels[name] = note.getLabelValue(name);
}
}
definitions.push({
const def: TableData = {
iconClass: note.getIcon(),
noteId: note.noteId,
title: note.title,
labels,
relations,
branchId: branch.branchId
});
branchId: branch.branchId,
}
return definitions;
if (note.hasChildren()) {
def._children = (await buildRowDefinitions(note, infos)).definitions;
hasSubtree = true;
}
export default function getPromotedAttributeInformation(parentNote: FNote) {
const info: PromotedAttributeInformation[] = [];
for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) {
const def = promotedAttribute.getDefinition();
definitions.push(def);
}
return {
definitions,
hasSubtree
};
}
export default function getAttributeDefinitionInformation(parentNote: FNote) {
const info: AttributeDefinitionInformation[] = [];
const attrDefs = parentNote.getAttributes()
.filter(attr => attr.isDefinition());
for (const attrDef of attrDefs) {
const def = attrDef.getDefinition();
if (def.multiplicity !== "single") {
console.warn("Multiple values are not supported for now");
continue;
}
const [ labelType, name ] = promotedAttribute.name.split(":", 2);
if (promotedAttribute.type !== "label") {
const [ labelType, name ] = attrDef.name.split(":", 2);
if (attrDef.type !== "label") {
console.warn("Relations are not supported for now");
continue;
}

View File

@ -1,4 +1,5 @@
import type { EventData } from "../../components/app_context.js";
import appContext from "../../components/app_context.js";
import Component from "../../components/component.js";
import type FNote from "../../entities/fnote.js";
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
@ -8,7 +9,6 @@ export interface ViewModeArgs {
$parent: JQuery<HTMLElement>;
parentNote: FNote;
parentNotePath?: string | null;
noteIds: string[];
showNotePath?: boolean;
}
@ -17,6 +17,8 @@ export default abstract class ViewMode<T extends object> extends Component {
private _viewStorage: ViewModeStorage<T> | null;
protected parentNote: FNote;
protected viewType: ViewTypeOptions;
protected noteIds: string[];
protected args: ViewModeArgs;
constructor(args: ViewModeArgs, viewType: ViewTypeOptions) {
super();
@ -25,6 +27,12 @@ export default abstract class ViewMode<T extends object> extends Component {
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
args.$parent.empty();
this.viewType = viewType;
this.args = args;
this.noteIds = [];
}
async beforeRender() {
await this.#refreshNoteIds();
}
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
@ -35,13 +43,18 @@ export default abstract class ViewMode<T extends object> extends Component {
* @param e the event data.
* @return {@code true} if the view should be re-rendered, a falsy value otherwise.
*/
onEntitiesReloaded(e: EventData<"entitiesReloaded">): boolean | void {
async onEntitiesReloaded(e: EventData<"entitiesReloaded">): Promise<boolean | void> {
// Do nothing by default.
}
get isFullHeight() {
// Override to change its value.
return false;
async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) {
if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) {
this.#refreshNoteIds();
}
if (await this.onEntitiesReloaded(e)) {
appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId });
}
}
get isReadOnly() {
@ -57,4 +70,14 @@ export default abstract class ViewMode<T extends object> extends Component {
return this._viewStorage;
}
async #refreshNoteIds() {
let noteIds: string[];
if (this.viewType === "list" || this.viewType === "grid") {
noteIds = this.args.parentNote.getChildNoteIds();
} else {
noteIds = await this.args.parentNote.getSubtreeNoteIds();
}
this.noteIds = noteIds;
}
}

View File

@ -3,12 +3,18 @@ import type { Router } from "express";
import fs from "fs";
import path from "path";
import { RESOURCE_DIR } from "../services/resource_dir";
import rateLimit from "express-rate-limit";
const specPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
let spec: string | null = null;
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
function register(router: Router) {
router.get("/etapi/etapi.openapi.yaml", (_, res) => {
router.get("/etapi/etapi.openapi.yaml", limiter, (_, res) => {
if (!spec) {
spec = fs.readFileSync(specPath, "utf8");
}