mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 06:54:23 +01:00
Merge branch 'main' into feat/add-ocr-capabilities
This commit is contained in:
commit
02980834ad
@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js";
|
|||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
import { StartupChecks } from "./startup_checks.js";
|
import { StartupChecks } from "./startup_checks.js";
|
||||||
|
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||||
|
import { ColumnComponent } from "tabulator-tables";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||||
@ -276,6 +278,17 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
geoMapCreateChildNote: CommandData;
|
geoMapCreateChildNote: CommandData;
|
||||||
|
|
||||||
|
// Table view
|
||||||
|
addNewRow: CommandData & {
|
||||||
|
customOpts: CreateNoteOpts;
|
||||||
|
parentNotePath?: string;
|
||||||
|
};
|
||||||
|
addNewTableColumn: CommandData & {
|
||||||
|
columnToEdit?: ColumnComponent;
|
||||||
|
referenceColumn?: ColumnComponent;
|
||||||
|
direction?: "before" | "after";
|
||||||
|
};
|
||||||
|
|
||||||
buildTouchBar: CommandData & {
|
buildTouchBar: CommandData & {
|
||||||
TouchBar: typeof TouchBar;
|
TouchBar: typeof TouchBar;
|
||||||
buildIcon(name: string): NativeImage;
|
buildIcon(name: string): NativeImage;
|
||||||
|
|||||||
@ -256,6 +256,15 @@ class FNote {
|
|||||||
return this.children;
|
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() {
|
async getChildNotes() {
|
||||||
return await this.froca.getNotes(this.children);
|
return await this.froca.getNotes(this.children);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||||
import type ViewMode 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 type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap";
|
||||||
|
|
||||||
export default class NoteListRenderer {
|
export default class NoteListRenderer {
|
||||||
|
|
||||||
private viewType: ViewTypeOptions;
|
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);
|
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 {
|
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||||
@ -47,15 +32,36 @@ export default class NoteListRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight() {
|
get isFullHeight() {
|
||||||
return this.viewMode?.isFullHeight;
|
switch (this.viewType) {
|
||||||
|
case "list":
|
||||||
|
case "grid":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderList() {
|
async renderList() {
|
||||||
if (!this.viewMode) {
|
const args = this.args;
|
||||||
return null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1967,7 +1967,11 @@
|
|||||||
"hide-column": "Hide column \"{{title}}\"",
|
"hide-column": "Hide column \"{{title}}\"",
|
||||||
"show-hide-columns": "Show/hide columns",
|
"show-hide-columns": "Show/hide columns",
|
||||||
"row-insert-above": "Insert row above",
|
"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": {
|
"book_properties_config": {
|
||||||
"hide-weekends": "Hide weekends",
|
"hide-weekends": "Hide weekends",
|
||||||
|
|||||||
@ -295,6 +295,7 @@ interface AttributeDetailOpts {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
focus?: "name";
|
focus?: "name";
|
||||||
|
parent?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchRelatedResponse {
|
interface SearchRelatedResponse {
|
||||||
@ -560,19 +561,22 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.toggleInt(true);
|
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 detPosition = this.getDetailPosition(x, offset);
|
||||||
const outerHeight = this.$widget.outerHeight();
|
const outerHeight = this.$widget.outerHeight();
|
||||||
const height = $(window).height();
|
const height = $(window).height();
|
||||||
|
|
||||||
if (detPosition && outerHeight && height) {
|
if (!detPosition || !outerHeight || !height) {
|
||||||
this.$widget
|
console.warn("Can't position popup, is it attached?");
|
||||||
.css("left", detPosition.left)
|
return;
|
||||||
.css("right", detPosition.right)
|
|
||||||
.css("top", y - offset.top + 70)
|
|
||||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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") {
|
if (focus === "name") {
|
||||||
this.$inputName.trigger("focus").trigger("select");
|
this.$inputName.trigger("focus").trigger("select");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -426,7 +426,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
|||||||
curNode = curNode.previousSibling;
|
curNode = curNode.previousSibling;
|
||||||
|
|
||||||
if ((curNode as ModelElement).name === "reference") {
|
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) {
|
} else if ("data" in curNode) {
|
||||||
clickIndex += (curNode.data as string).length;
|
clickIndex += (curNode.data as string).length;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import NoteListRenderer from "../services/note_list_renderer.js";
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
|
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
|
||||||
import type ViewMode from "./view_widgets/view_mode.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*/`
|
const TPL = /*html*/`
|
||||||
<div class="note-list-widget">
|
<div class="note-list-widget">
|
||||||
@ -39,7 +37,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
private noteIdRefreshed?: string;
|
private noteIdRefreshed?: string;
|
||||||
private shownNoteId?: string | null;
|
private shownNoteId?: string | null;
|
||||||
private viewMode?: ViewMode<any> | null;
|
private viewMode?: ViewMode<any> | null;
|
||||||
private attributeDetailWidget: AttributeDetailWidget;
|
|
||||||
private displayOnlyCollections: boolean;
|
private displayOnlyCollections: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,9 +44,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
*/
|
*/
|
||||||
constructor(displayOnlyCollections: boolean) {
|
constructor(displayOnlyCollections: boolean) {
|
||||||
super();
|
super();
|
||||||
this.attributeDetailWidget = new AttributeDetailWidget()
|
|
||||||
.contentSized()
|
|
||||||
.setParent(this);
|
|
||||||
this.displayOnlyCollections = displayOnlyCollections;
|
this.displayOnlyCollections = displayOnlyCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +67,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.contentSized();
|
this.contentSized();
|
||||||
this.$content = this.$widget.find(".note-list-widget-content");
|
this.$content = this.$widget.find(".note-list-widget-content");
|
||||||
this.$widget.append(this.attributeDetailWidget.render());
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
@ -91,23 +85,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
setTimeout(() => observer.observe(this.$widget[0]), 10);
|
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() {
|
checkRenderStatus() {
|
||||||
// console.log("this.isIntersecting", this.isIntersecting);
|
// console.log("this.isIntersecting", this.isIntersecting);
|
||||||
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
|
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
|
||||||
@ -123,8 +100,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
const noteListRenderer = new NoteListRenderer({
|
const noteListRenderer = new NoteListRenderer({
|
||||||
$parent: this.$content,
|
$parent: this.$content,
|
||||||
parentNote: note,
|
parentNote: note,
|
||||||
parentNotePath: this.notePath,
|
parentNotePath: this.notePath
|
||||||
noteIds: note.getChildNoteIds()
|
|
||||||
});
|
});
|
||||||
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||||
await noteListRenderer.renderList();
|
await noteListRenderer.renderList();
|
||||||
@ -169,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
this.checkRenderStatus();
|
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">) {
|
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
|
||||||
|
|||||||
@ -68,7 +68,6 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
|
|||||||
const noteListRenderer = new NoteListRenderer({
|
const noteListRenderer = new NoteListRenderer({
|
||||||
$parent: this.$content,
|
$parent: this.$content,
|
||||||
parentNote: note,
|
parentNote: note,
|
||||||
noteIds: note.getChildNoteIds(),
|
|
||||||
showNotePath: true
|
showNotePath: true
|
||||||
});
|
});
|
||||||
await noteListRenderer.renderList();
|
await noteListRenderer.renderList();
|
||||||
|
|||||||
@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) {
|
|||||||
const templateNoteIds = new Set(templateCache.keys());
|
const templateNoteIds = new Set(templateCache.keys());
|
||||||
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
|
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
|
||||||
|
|
||||||
await froca.getNotes(affectedNoteIds);
|
await froca.getNotes(affectedNoteIds, true);
|
||||||
|
|
||||||
let fullReloadNeeded = false;
|
let fullReloadNeeded = false;
|
||||||
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
|
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
|
||||||
|
|||||||
@ -265,7 +265,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$editor.trigger("focus");
|
const editor = this.watchdog.editor;
|
||||||
|
if (editor) {
|
||||||
|
editor.editing.view.focus();
|
||||||
|
} else {
|
||||||
|
this.$editor.trigger("focus");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToEnd() {
|
scrollToEnd() {
|
||||||
|
|||||||
@ -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}
|
* {@inheritdoc}
|
||||||
*
|
*
|
||||||
|
|||||||
@ -113,7 +113,6 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
|
|
||||||
private $root: JQuery<HTMLElement>;
|
private $root: JQuery<HTMLElement>;
|
||||||
private $calendarContainer: JQuery<HTMLElement>;
|
private $calendarContainer: JQuery<HTMLElement>;
|
||||||
private noteIds: string[];
|
|
||||||
private calendar?: Calendar;
|
private calendar?: Calendar;
|
||||||
private isCalendarRoot: boolean;
|
private isCalendarRoot: boolean;
|
||||||
private lastView?: string;
|
private lastView?: string;
|
||||||
@ -124,15 +123,10 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
|
|
||||||
this.$root = $(TPL);
|
this.$root = $(TPL);
|
||||||
this.$calendarContainer = this.$root.find(".calendar-container");
|
this.$calendarContainer = this.$root.find(".calendar-container");
|
||||||
this.noteIds = args.noteIds;
|
|
||||||
this.isCalendarRoot = false;
|
this.isCalendarRoot = false;
|
||||||
args.$parent.append(this.$root);
|
args.$parent.append(this.$root);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
||||||
this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
|
this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
|
||||||
const isEditable = !this.isCalendarRoot;
|
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.
|
// Refresh note IDs if they got changed.
|
||||||
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
||||||
this.noteIds = this.parentNote.getChildNoteIds();
|
this.noteIds = this.parentNote.getChildNoteIds();
|
||||||
@ -438,7 +432,7 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
events.push(await CalendarView.buildEvent(dateNote, { startDate }));
|
events.push(await CalendarView.buildEvent(dateNote, { startDate }));
|
||||||
|
|
||||||
if (dateNote.hasChildren()) {
|
if (dateNote.hasChildren()) {
|
||||||
const childNoteIds = dateNote.getChildNoteIds();
|
const childNoteIds = await dateNote.getSubtreeNoteIds();
|
||||||
for (const childNoteId of childNoteIds) {
|
for (const childNoteId of childNoteIds) {
|
||||||
childNoteToDateMapping[childNoteId] = startDate;
|
childNoteToDateMapping[childNoteId] = startDate;
|
||||||
}
|
}
|
||||||
@ -464,13 +458,6 @@ export default class CalendarView extends ViewMode<{}> {
|
|||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate");
|
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) {
|
if (!startDate) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -243,10 +243,6 @@ export default class GeoView extends ViewMode<MapData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changeState(newState: State) {
|
#changeState(newState: State) {
|
||||||
this._state = newState;
|
this._state = newState;
|
||||||
this.$container.toggleClass("placing-note", newState === State.NewNote);
|
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 any of the children branches are altered.
|
||||||
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
||||||
this.#reloadMarkers();
|
this.#reloadMarkers();
|
||||||
|
|||||||
@ -161,7 +161,7 @@ const TPL = /*html*/`
|
|||||||
class ListOrGridView extends ViewMode<{}> {
|
class ListOrGridView extends ViewMode<{}> {
|
||||||
private $noteList: JQuery<HTMLElement>;
|
private $noteList: JQuery<HTMLElement>;
|
||||||
|
|
||||||
private noteIds: string[];
|
private filteredNoteIds!: string[];
|
||||||
private page?: number;
|
private page?: number;
|
||||||
private pageSize?: number;
|
private pageSize?: number;
|
||||||
private showNotePath?: boolean;
|
private showNotePath?: boolean;
|
||||||
@ -174,13 +174,6 @@ class ListOrGridView extends ViewMode<{}> {
|
|||||||
super(args, viewType);
|
super(args, viewType);
|
||||||
this.$noteList = $(TPL);
|
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);
|
args.$parent.append(this.$noteList);
|
||||||
|
|
||||||
@ -204,8 +197,14 @@ class ListOrGridView extends ViewMode<{}> {
|
|||||||
return new Set(includedLinks.map((rel) => rel.value));
|
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() {
|
async renderList() {
|
||||||
if (this.noteIds.length === 0 || !this.page || !this.pageSize) {
|
if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) {
|
||||||
this.$noteList.hide();
|
this.$noteList.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -226,7 +225,7 @@ class ListOrGridView extends ViewMode<{}> {
|
|||||||
const startIdx = (this.page - 1) * this.pageSize;
|
const startIdx = (this.page - 1) * this.pageSize;
|
||||||
const endIdx = startIdx + 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);
|
const pageNotes = await froca.getNotes(pageNoteIds);
|
||||||
|
|
||||||
for (const note of pageNotes) {
|
for (const note of pageNotes) {
|
||||||
@ -246,7 +245,7 @@ class ListOrGridView extends ViewMode<{}> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
|
const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize);
|
||||||
|
|
||||||
$pager.toggle(pageCount > 1);
|
$pager.toggle(pageCount > 1);
|
||||||
|
|
||||||
@ -257,7 +256,7 @@ class ListOrGridView extends ViewMode<{}> {
|
|||||||
lastPrinted = true;
|
lastPrinted = true;
|
||||||
|
|
||||||
const startIndex = (i - 1) * this.pageSize + 1;
|
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(
|
$pager.append(
|
||||||
i === this.page
|
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
|
// 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) {
|
async renderNote(note: FNote, expand: boolean = false) {
|
||||||
|
|||||||
104
apps/client/src/widgets/view_widgets/table_view/col_editing.ts
Normal file
104
apps/client/src/widgets/view_widgets/table_view/col_editing.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import { RelationEditor } from "./relation_editor.js";
|
import { RelationEditor } from "./relation_editor.js";
|
||||||
import { NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
|
import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
|
||||||
import { applyHeaderMenu } from "./header-menu.js";
|
|
||||||
import type { ColumnDefinition } from "tabulator-tables";
|
import type { ColumnDefinition } from "tabulator-tables";
|
||||||
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
||||||
|
|
||||||
type ColumnType = LabelType | "relation";
|
type ColumnType = LabelType | "relation";
|
||||||
|
|
||||||
export interface PromotedAttributeInformation {
|
export interface AttributeDefinitionInformation {
|
||||||
name: string;
|
name: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: ColumnType;
|
type?: ColumnType;
|
||||||
@ -42,20 +41,21 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) {
|
export function buildColumnDefinitions(info: AttributeDefinitionInformation[], movableRows: boolean, existingColumnData?: ColumnDefinition[], position?: number) {
|
||||||
const columnDefs: ColumnDefinition[] = [
|
let columnDefs: ColumnDefinition[] = [
|
||||||
{
|
{
|
||||||
title: "#",
|
title: "#",
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
hozAlign: "center",
|
hozAlign: "center",
|
||||||
resizable: false,
|
resizable: false,
|
||||||
frozen: true,
|
frozen: true,
|
||||||
rowHandle: true,
|
rowHandle: movableRows,
|
||||||
formatter: RowNumberFormatter
|
formatter: RowNumberFormatter(movableRows)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "noteId",
|
field: "noteId",
|
||||||
title: "Note ID",
|
title: "Note ID",
|
||||||
|
formatter: MonospaceFormatter,
|
||||||
visible: false
|
visible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -86,27 +86,41 @@ export function buildColumnDefinitions(info: PromotedAttributeInformation[], exi
|
|||||||
seenFields.add(field);
|
seenFields.add(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyHeaderMenu(columnDefs);
|
|
||||||
if (existingColumnData) {
|
if (existingColumnData) {
|
||||||
restoreExistingData(columnDefs, existingColumnData);
|
columnDefs = restoreExistingData(columnDefs, existingColumnData, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
return columnDefs;
|
return columnDefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) {
|
export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) {
|
||||||
const byField = new Map<string, ColumnDefinition>;
|
// 1. Keep existing columns, but restore their properties like width, visibility and order.
|
||||||
for (const def of oldDefs) {
|
const newItemsByField = new Map<string, ColumnDefinition>(
|
||||||
byField.set(def.field ?? "", def);
|
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) {
|
// 2. Determine new columns.
|
||||||
const oldDef = byField.get(newDef.field ?? "");
|
const existingFields = new Set(existingColumns.map(item => item.field));
|
||||||
if (!oldDef) {
|
const newColumns = newDefs
|
||||||
continue;
|
.filter(item => !existingFields.has(item.field!));
|
||||||
}
|
|
||||||
|
|
||||||
newDef.width = oldDef.width;
|
// Clamp position to a valid range
|
||||||
newDef.visible = oldDef.visible;
|
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)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,19 @@ import branches from "../../../services/branches.js";
|
|||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import link_context_menu from "../../../menus/link_context_menu.js";
|
import link_context_menu from "../../../menus/link_context_menu.js";
|
||||||
import type FNote from "../../../entities/fnote.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) {
|
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));
|
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) {
|
function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator: Tabulator) {
|
||||||
@ -69,6 +78,29 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator:
|
|||||||
uiIcon: "bx bx-empty",
|
uiIcon: "bx bx-empty",
|
||||||
items: buildColumnItems()
|
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() {},
|
selectMenuItemHandler() {},
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
@ -79,11 +111,11 @@ function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, tabulator:
|
|||||||
function buildColumnItems() {
|
function buildColumnItems() {
|
||||||
const items: MenuItem<unknown>[] = [];
|
const items: MenuItem<unknown>[] = [];
|
||||||
for (const column of tabulator.getColumns()) {
|
for (const column of tabulator.getColumns()) {
|
||||||
const { title, visible, field } = column.getDefinition();
|
const { title, field } = column.getDefinition();
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
title,
|
title,
|
||||||
checked: visible,
|
checked: column.isVisible(),
|
||||||
uiIcon: "bx bx-empty",
|
uiIcon: "bx bx-empty",
|
||||||
enabled: !!field,
|
enabled: !!field,
|
||||||
handler: () => column.toggle()
|
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 e = _e as MouseEvent;
|
||||||
const rowData = row.getData() as TableData;
|
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({
|
contextMenu.show({
|
||||||
items: [
|
items: [
|
||||||
...link_context_menu.getItems(),
|
...link_context_menu.getItems(),
|
||||||
@ -104,16 +146,25 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
|
|||||||
{
|
{
|
||||||
title: t("table_view.row-insert-above"),
|
title: t("table_view.row-insert-above"),
|
||||||
uiIcon: "bx bx-list-plus",
|
uiIcon: "bx bx-list-plus",
|
||||||
handler: () => {
|
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
|
||||||
const target = e.target;
|
parentNotePath: parentNoteId,
|
||||||
if (!target) {
|
customOpts: {
|
||||||
return;
|
target: "before",
|
||||||
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
const component = $(target).closest(".component").prop("component");
|
})
|
||||||
component.triggerCommand("addNewRow", {
|
},
|
||||||
|
{
|
||||||
|
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: {
|
customOpts: {
|
||||||
target: "before",
|
target: "after",
|
||||||
targetBranchId: rowData.branchId,
|
targetBranchId: branchId,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -121,19 +172,13 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
|
|||||||
{
|
{
|
||||||
title: t("table_view.row-insert-below"),
|
title: t("table_view.row-insert-below"),
|
||||||
uiIcon: "bx bx-empty",
|
uiIcon: "bx bx-empty",
|
||||||
handler: () => {
|
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
|
||||||
const target = e.target;
|
parentNotePath: parentNoteId,
|
||||||
if (!target) {
|
customOpts: {
|
||||||
return;
|
target: "after",
|
||||||
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
const component = $(target).closest(".component").prop("component");
|
})
|
||||||
component.triggerCommand("addNewRow", {
|
|
||||||
customOpts: {
|
|
||||||
target: "after",
|
|
||||||
targetBranchId: rowData.branchId,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
{
|
{
|
||||||
@ -148,3 +193,13 @@ export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: F
|
|||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getParentComponent(e: MouseEvent) {
|
||||||
|
if (!e.target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $(e.target)
|
||||||
|
.closest(".component")
|
||||||
|
.prop("component") as Component;
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function buildFooter(parentNote: FNote) {
|
|||||||
<span class="bx bx-plus"></span> ${t("table_view.new-row")}
|
<span class="bx bx-plus"></span> ${t("table_view.new-row")}
|
||||||
</button>
|
</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")}
|
<span class="bx bx-columns"></span> ${t("table_view.new-column")}
|
||||||
</button>
|
</button>
|
||||||
`.trimStart();
|
`.trimStart();
|
||||||
|
|||||||
@ -36,8 +36,19 @@ export function NoteTitleFormatter(cell: CellComponent) {
|
|||||||
return $noteRef[0].outerHTML;
|
return $noteRef[0].outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RowNumberFormatter(cell: CellComponent) {
|
export function RowNumberFormatter(draggableRows: boolean) {
|
||||||
return `<span class="bx bx-dots-vertical-rounded"></span> ` + cell.getRow().getPosition(true);
|
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) {
|
function buildNoteLink(noteId: string) {
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import froca from "../../../services/froca.js";
|
|
||||||
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
|
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
|
||||||
import attributes, { setAttribute, setLabel } from "../../../services/attributes.js";
|
import attributes from "../../../services/attributes.js";
|
||||||
import server from "../../../services/server.js";
|
|
||||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||||
import type { CommandListenerData, EventData } from "../../../components/app_context.js";
|
import type { EventData } from "../../../components/app_context.js";
|
||||||
import type { Attribute } from "../../../services/attribute_parser.js";
|
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables';
|
||||||
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 "tabulator-tables/dist/css/tabulator.css";
|
import "tabulator-tables/dist/css/tabulator.css";
|
||||||
import "../../../../src/stylesheets/table.css";
|
import "../../../../src/stylesheets/table.css";
|
||||||
import { canReorderRows, configureReorderingRows } from "./dragging.js";
|
import { canReorderRows, configureReorderingRows } from "./dragging.js";
|
||||||
import buildFooter from "./footer.js";
|
import buildFooter from "./footer.js";
|
||||||
import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js";
|
import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js";
|
||||||
import { buildColumnDefinitions } from "./columns.js";
|
import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js";
|
||||||
import { setupContextMenu } from "./context_menu.js";
|
import { setupContextMenu } from "./context_menu.js";
|
||||||
|
import TableColumnEditing from "./col_editing.js";
|
||||||
|
import TableRowEditing from "./row_editing.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="table-view">
|
<div class="table-view">
|
||||||
@ -65,6 +63,26 @@ const TPL = /*html*/`
|
|||||||
justify-content: left;
|
justify-content: left;
|
||||||
gap: 0.5em;
|
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>
|
</style>
|
||||||
|
|
||||||
<div class="table-view-container"></div>
|
<div class="table-view-container"></div>
|
||||||
@ -81,29 +99,22 @@ export default class TableView extends ViewMode<StateInfo> {
|
|||||||
|
|
||||||
private $root: JQuery<HTMLElement>;
|
private $root: JQuery<HTMLElement>;
|
||||||
private $container: JQuery<HTMLElement>;
|
private $container: JQuery<HTMLElement>;
|
||||||
private args: ViewModeArgs;
|
|
||||||
private spacedUpdate: SpacedUpdate;
|
private spacedUpdate: SpacedUpdate;
|
||||||
private api?: Tabulator;
|
private api?: Tabulator;
|
||||||
private newAttribute?: Attribute;
|
|
||||||
private persistentData: StateInfo["tableData"];
|
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 colEditing?: TableColumnEditing;
|
||||||
private noteIdToEdit?: string;
|
private rowEditing?: TableRowEditing;
|
||||||
|
|
||||||
constructor(args: ViewModeArgs) {
|
constructor(args: ViewModeArgs) {
|
||||||
super(args, "table");
|
super(args, "table");
|
||||||
|
|
||||||
this.$root = $(TPL);
|
this.$root = $(TPL);
|
||||||
this.$container = this.$root.find(".table-view-container");
|
this.$container = this.$root.find(".table-view-container");
|
||||||
this.args = args;
|
|
||||||
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
|
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
|
||||||
this.persistentData = {};
|
this.persistentData = {};
|
||||||
args.$parent.append(this.$root);
|
args.$parent.append(this.$root);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderList() {
|
async renderList() {
|
||||||
this.$container.empty();
|
this.$container.empty();
|
||||||
this.renderTable(this.$container[0]);
|
this.renderTable(this.$container[0]);
|
||||||
@ -111,29 +122,27 @@ export default class TableView extends ViewMode<StateInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async renderTable(el: HTMLElement) {
|
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) {
|
for (const module of modules) {
|
||||||
Tabulator.registerModule(module);
|
Tabulator.registerModule(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initialize(el);
|
this.initialize(el, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initialize(el: HTMLElement) {
|
private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) {
|
||||||
const notes = await froca.getNotes(this.args.noteIds);
|
|
||||||
const info = getPromotedAttributeInformation(this.parentNote);
|
|
||||||
|
|
||||||
const viewStorage = await this.viewStorage.restore();
|
const viewStorage = await this.viewStorage.restore();
|
||||||
this.persistentData = viewStorage?.tableData || {};
|
this.persistentData = viewStorage?.tableData || {};
|
||||||
|
|
||||||
const columnDefs = buildColumnDefinitions(info);
|
const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info);
|
||||||
const movableRows = canReorderRows(this.parentNote);
|
const movableRows = canReorderRows(this.parentNote) && !hasChildren;
|
||||||
|
const columnDefs = buildColumnDefinitions(info, movableRows);
|
||||||
this.api = new Tabulator(el, {
|
let opts: Options = {
|
||||||
layout: "fitDataFill",
|
layout: "fitDataFill",
|
||||||
index: "noteId",
|
index: "branchId",
|
||||||
columns: columnDefs,
|
columns: columnDefs,
|
||||||
data: await buildRowDefinitions(this.parentNote, notes, info),
|
data: rowData,
|
||||||
persistence: true,
|
persistence: true,
|
||||||
movableColumns: true,
|
movableColumns: true,
|
||||||
movableRows,
|
movableRows,
|
||||||
@ -143,10 +152,28 @@ export default class TableView extends ViewMode<StateInfo> {
|
|||||||
this.spacedUpdate.scheduleUpdate();
|
this.spacedUpdate.scheduleUpdate();
|
||||||
},
|
},
|
||||||
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
|
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
|
||||||
});
|
};
|
||||||
configureReorderingRows(this.api);
|
|
||||||
|
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);
|
setupContextMenu(this.api, this.parentNote);
|
||||||
this.setupEditing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSave() {
|
private onSave() {
|
||||||
@ -155,86 +182,29 @@ export default class TableView extends ViewMode<StateInfo> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEditing() {
|
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
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 {
|
|
||||||
if (!this.api) {
|
if (!this.api) {
|
||||||
return;
|
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.
|
// Refresh if promoted attributes get changed.
|
||||||
if (loadResults.getAttributeRows().find(attr =>
|
if (loadResults.getAttributeRows().find(attr =>
|
||||||
attr.type === "label" &&
|
attr.type === "label" &&
|
||||||
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
|
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
|
||||||
attributes.isAffecting(attr, this.parentNote))) {
|
attributes.isAffecting(attr, this.parentNote))) {
|
||||||
|
console.log("Col update");
|
||||||
this.#manageColumnUpdate();
|
this.#manageColumnUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)
|
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))
|
||||||
|| loadResults.getNoteIds().some(noteId => this.args.noteIds.includes(noteId)
|
|| loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)
|
||||||
|| loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!)))) {
|
|| loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!)))) {
|
||||||
this.#manageRowsUpdate();
|
return await this.#manageRowsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -245,27 +215,44 @@ export default class TableView extends ViewMode<StateInfo> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = getPromotedAttributeInformation(this.parentNote);
|
const info = getAttributeDefinitionInformation(this.parentNote);
|
||||||
const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns);
|
const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.colEditing?.getNewAttributePosition());
|
||||||
this.api.setColumns(columnDefs);
|
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() {
|
async #manageRowsUpdate() {
|
||||||
if (!this.api) {
|
if (!this.api) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notes = await froca.getNotes(this.args.noteIds);
|
const info = getAttributeDefinitionInformation(this.parentNote);
|
||||||
const info = getPromotedAttributeInformation(this.parentNote);
|
const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info);
|
||||||
this.api.replaceData(await buildRowDefinitions(this.parentNote, notes, info));
|
|
||||||
|
|
||||||
if (this.noteIdToEdit) {
|
// Force a refresh if the data tree needs enabling/disabling.
|
||||||
const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit);
|
if (this.api.options.dataTree !== hasSubtree) {
|
||||||
if (row) {
|
return true;
|
||||||
row.getCell("title").edit();
|
|
||||||
}
|
|
||||||
this.noteIdToEdit = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.api.replaceData(definitions);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import FNote from "../../../entities/fnote.js";
|
import FNote from "../../../entities/fnote.js";
|
||||||
import type { LabelType } from "../../../services/promoted_attribute_definition_parser.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 = {
|
export type TableData = {
|
||||||
iconClass: string;
|
iconClass: string;
|
||||||
@ -9,10 +9,12 @@ export type TableData = {
|
|||||||
labels: Record<string, boolean | string | null>;
|
labels: Record<string, boolean | string | null>;
|
||||||
relations: Record<string, boolean | string | null>;
|
relations: Record<string, boolean | string | null>;
|
||||||
branchId: string;
|
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[] = [];
|
const definitions: TableData[] = [];
|
||||||
|
let hasSubtree = false;
|
||||||
for (const branch of parentNote.getChildBranches()) {
|
for (const branch of parentNote.getChildBranches()) {
|
||||||
const note = await branch.getNote();
|
const note = await branch.getNote();
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -28,30 +30,43 @@ export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], inf
|
|||||||
labels[name] = note.getLabelValue(name);
|
labels[name] = note.getLabelValue(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
definitions.push({
|
|
||||||
|
const def: TableData = {
|
||||||
iconClass: note.getIcon(),
|
iconClass: note.getIcon(),
|
||||||
noteId: note.noteId,
|
noteId: note.noteId,
|
||||||
title: note.title,
|
title: note.title,
|
||||||
labels,
|
labels,
|
||||||
relations,
|
relations,
|
||||||
branchId: branch.branchId
|
branchId: branch.branchId,
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (note.hasChildren()) {
|
||||||
|
def._children = (await buildRowDefinitions(note, infos)).definitions;
|
||||||
|
hasSubtree = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
definitions.push(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
return definitions;
|
return {
|
||||||
|
definitions,
|
||||||
|
hasSubtree
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function getPromotedAttributeInformation(parentNote: FNote) {
|
export default function getAttributeDefinitionInformation(parentNote: FNote) {
|
||||||
const info: PromotedAttributeInformation[] = [];
|
const info: AttributeDefinitionInformation[] = [];
|
||||||
for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) {
|
const attrDefs = parentNote.getAttributes()
|
||||||
const def = promotedAttribute.getDefinition();
|
.filter(attr => attr.isDefinition());
|
||||||
|
for (const attrDef of attrDefs) {
|
||||||
|
const def = attrDef.getDefinition();
|
||||||
if (def.multiplicity !== "single") {
|
if (def.multiplicity !== "single") {
|
||||||
console.warn("Multiple values are not supported for now");
|
console.warn("Multiple values are not supported for now");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ labelType, name ] = promotedAttribute.name.split(":", 2);
|
const [ labelType, name ] = attrDef.name.split(":", 2);
|
||||||
if (promotedAttribute.type !== "label") {
|
if (attrDef.type !== "label") {
|
||||||
console.warn("Relations are not supported for now");
|
console.warn("Relations are not supported for now");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import appContext from "../../components/app_context.js";
|
||||||
import Component from "../../components/component.js";
|
import Component from "../../components/component.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||||
@ -8,7 +9,6 @@ export interface ViewModeArgs {
|
|||||||
$parent: JQuery<HTMLElement>;
|
$parent: JQuery<HTMLElement>;
|
||||||
parentNote: FNote;
|
parentNote: FNote;
|
||||||
parentNotePath?: string | null;
|
parentNotePath?: string | null;
|
||||||
noteIds: string[];
|
|
||||||
showNotePath?: boolean;
|
showNotePath?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +17,8 @@ export default abstract class ViewMode<T extends object> extends Component {
|
|||||||
private _viewStorage: ViewModeStorage<T> | null;
|
private _viewStorage: ViewModeStorage<T> | null;
|
||||||
protected parentNote: FNote;
|
protected parentNote: FNote;
|
||||||
protected viewType: ViewTypeOptions;
|
protected viewType: ViewTypeOptions;
|
||||||
|
protected noteIds: string[];
|
||||||
|
protected args: ViewModeArgs;
|
||||||
|
|
||||||
constructor(args: ViewModeArgs, viewType: ViewTypeOptions) {
|
constructor(args: ViewModeArgs, viewType: ViewTypeOptions) {
|
||||||
super();
|
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
|
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
|
||||||
args.$parent.empty();
|
args.$parent.empty();
|
||||||
this.viewType = viewType;
|
this.viewType = viewType;
|
||||||
|
this.args = args;
|
||||||
|
this.noteIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeRender() {
|
||||||
|
await this.#refreshNoteIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
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.
|
* @param e the event data.
|
||||||
* @return {@code true} if the view should be re-rendered, a falsy value otherwise.
|
* @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.
|
// Do nothing by default.
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight() {
|
async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) {
|
||||||
// Override to change its value.
|
if (e.loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))) {
|
||||||
return false;
|
this.#refreshNoteIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.onEntitiesReloaded(e)) {
|
||||||
|
appContext.triggerEvent("refreshNoteList", { noteId: this.parentNote.noteId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isReadOnly() {
|
get isReadOnly() {
|
||||||
@ -57,4 +70,14 @@ export default abstract class ViewMode<T extends object> extends Component {
|
|||||||
return this._viewStorage;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,18 @@ import type { Router } from "express";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { RESOURCE_DIR } from "../services/resource_dir";
|
import { RESOURCE_DIR } from "../services/resource_dir";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
|
|
||||||
const specPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
|
const specPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
|
||||||
let spec: string | null = null;
|
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) {
|
function register(router: Router) {
|
||||||
router.get("/etapi/etapi.openapi.yaml", (_, res) => {
|
router.get("/etapi/etapi.openapi.yaml", limiter, (_, res) => {
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
spec = fs.readFileSync(specPath, "utf8");
|
spec = fs.readFileSync(specPath, "utf8");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user