mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
Introduce the table view (#6097)
This commit is contained in:
commit
ac8b0535d2
@ -54,6 +54,7 @@
|
||||
"preact": "10.26.9",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -63,6 +64,7 @@
|
||||
"@types/leaflet": "1.9.19",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/tabulator-tables": "6.2.6",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
|
@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
|
||||
if (fun) {
|
||||
return this.callMethod(fun, data);
|
||||
} else {
|
||||
if (!this.parent) {
|
||||
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
|
||||
}
|
||||
|
||||
} else if (this.parent) {
|
||||
return this.parent.triggerCommand(name, data);
|
||||
}
|
||||
}
|
||||
|
@ -315,14 +315,38 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
|
||||
hasNoteList() {
|
||||
return (
|
||||
this.note &&
|
||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
||||
["book", "text", "code"].includes(this.note.type) &&
|
||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
||||
);
|
||||
const note = this.note;
|
||||
|
||||
if (!note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some book types must always display a note list, even if no children.
|
||||
if (["calendar", "table"].includes(note.getLabelValue("viewType") ?? "")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!note.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["book", "text", "code"].includes(note.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.isLabelTruthy("hideChildrenOverview")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||
|
@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T> {
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
@ -28,6 +28,7 @@ export interface MenuCommandItem<T> {
|
||||
items?: MenuItem<T>[] | null;
|
||||
shortcut?: string;
|
||||
spellingSuggestion?: string;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
@ -146,11 +147,14 @@ class ContextMenu {
|
||||
} else {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if ("uiIcon" in item && item.uiIcon) {
|
||||
$icon.addClass(item.uiIcon);
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
const $link = $("<span>")
|
||||
.append($icon)
|
||||
|
@ -3,15 +3,16 @@ import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "") {
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
async function setLabel(noteId: string, name: string, value: string = "") {
|
||||
export async function setLabel(noteId: string, name: string, value: string = "") {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
@ -49,7 +50,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||
* @param name the name of the attribute to set.
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
|
@ -118,8 +118,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
||||
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
let content = blob?.content || "";
|
||||
if (note.mime === "application/json") {
|
||||
try {
|
||||
content = JSON.stringify(JSON.parse(content), null, 4);
|
||||
} catch (e) {
|
||||
// Ignore JSON parsing errors.
|
||||
}
|
||||
}
|
||||
|
||||
const $codeBlock = $("<code>");
|
||||
$codeBlock.text(blob?.content || "");
|
||||
$codeBlock.text(content);
|
||||
$renderedContent.append($("<pre>").append($codeBlock));
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
@ -301,7 +310,7 @@ function getRenderingType(entity: FNote | FAttachment) {
|
||||
|
||||
if (type === "file" && mime === "application/pdf") {
|
||||
type = "pdf";
|
||||
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
type = "code";
|
||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
|
@ -384,7 +384,7 @@ function linkContextMenu(e: PointerEvent) {
|
||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
||||
}
|
||||
|
||||
async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
|
||||
export async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
|
||||
const $link = $el[0].tagName === "A" ? $el : $el.find("a");
|
||||
|
||||
href = href || $link.attr("href");
|
||||
|
@ -1,30 +1,32 @@
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
|
||||
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 ViewTypeOptions = "list" | "grid" | "calendar";
|
||||
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table";
|
||||
|
||||
export default class NoteListRenderer {
|
||||
|
||||
private viewType: ViewTypeOptions;
|
||||
public viewMode: ViewMode | null;
|
||||
public viewMode: ViewMode<any> | null;
|
||||
|
||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
|
||||
this.viewType = this.#getViewType(parentNote);
|
||||
const args: ViewModeArgs = {
|
||||
$parent,
|
||||
parentNote,
|
||||
noteIds,
|
||||
showNotePath
|
||||
};
|
||||
constructor(args: ViewModeArgs) {
|
||||
this.viewType = this.#getViewType(args.parentNote);
|
||||
|
||||
if (this.viewType === "list" || this.viewType === "grid") {
|
||||
switch (this.viewType) {
|
||||
case "list":
|
||||
case "grid":
|
||||
this.viewMode = new ListOrGridView(this.viewType, args);
|
||||
} else if (this.viewType === "calendar") {
|
||||
break;
|
||||
case "calendar":
|
||||
this.viewMode = new CalendarView(args);
|
||||
} else {
|
||||
break;
|
||||
case "table":
|
||||
this.viewMode = new TableView(args);
|
||||
break;
|
||||
default:
|
||||
this.viewMode = null;
|
||||
}
|
||||
}
|
||||
@ -32,7 +34,7 @@ export default class NoteListRenderer {
|
||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||
const viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(viewType || "")) {
|
||||
if (!["list", "grid", "calendar", "table"].includes(viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
return parentNote.type === "search" ? "list" : "grid";
|
||||
} else {
|
||||
|
@ -14,6 +14,7 @@ let dismissTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
$(document).on("mouseenter", "[data-href]", mouseEnterHandler);
|
||||
|
||||
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
||||
$(document).on("click", (e) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
export interface DefinitionObject {
|
||||
|
@ -760,7 +760,8 @@
|
||||
"expand": "Expand",
|
||||
"book_properties": "Book Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar"
|
||||
"calendar": "Calendar",
|
||||
"table": "Table"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@ -1933,5 +1934,9 @@
|
||||
"title": "Features",
|
||||
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
||||
"note_completion_enabled": "Enable note auto-completion"
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "New row",
|
||||
"new-column": "New column"
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: null,
|
||||
grid: null,
|
||||
calendar: "xWbu3jpNWapp"
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29"
|
||||
};
|
||||
|
||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
|
@ -1,8 +1,10 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { CommandListener, CommandListenerData, EventData } from "../components/app_context.js";
|
||||
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData } 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">
|
||||
@ -36,7 +38,15 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
private isIntersecting?: boolean;
|
||||
private noteIdRefreshed?: string;
|
||||
private shownNoteId?: string | null;
|
||||
private viewMode?: ViewMode | null;
|
||||
private viewMode?: ViewMode<any> | null;
|
||||
private attributeDetailWidget: AttributeDetailWidget;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attributeDetailWidget = new AttributeDetailWidget()
|
||||
.contentSized()
|
||||
.setParent(this);
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext?.hasNoteList();
|
||||
@ -46,6 +56,7 @@ 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) => {
|
||||
@ -64,6 +75,23 @@ 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);
|
||||
@ -76,7 +104,12 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async renderNoteList(note: FNote) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
parentNotePath: this.notePath,
|
||||
noteIds: note.getChildNoteIds()
|
||||
});
|
||||
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||
await noteListRenderer.renderList();
|
||||
this.viewMode = noteListRenderer.viewMode;
|
||||
@ -134,4 +167,13 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
triggerCommand<K extends CommandNames>(name: K, data?: CommandMappings[K]): Promise<unknown> | undefined | null {
|
||||
// Pass the commands to the view mode, which is not actually attached to the hierarchy.
|
||||
if (this.viewMode?.triggerCommand(name, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return super.triggerCommand(name, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ const TPL = /*html*/`
|
||||
<option value="grid">${t("book_properties.grid")}</option>
|
||||
<option value="list">${t("book_properties.list")}</option>
|
||||
<option value="calendar">${t("book_properties.calendar")}</option>
|
||||
<option value="table">${t("book_properties.table")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -67,7 +68,6 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: t("book_properties.book_properties"),
|
||||
icon: "bx bx-book"
|
||||
};
|
||||
@ -126,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(type)) {
|
||||
if (!["list", "grid", "calendar", "table"].includes(type)) {
|
||||
throw new Error(t("book_properties.invalid_view_type", { type }));
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
// the order of attributes is important as well
|
||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
|
||||
this.toggleInt(false);
|
||||
return;
|
||||
}
|
||||
|
@ -65,7 +65,13 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true);
|
||||
// this.$content, note, note.getChildNoteIds(), true
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
noteIds: note.getChildNoteIds(),
|
||||
showNotePath: true
|
||||
});
|
||||
await noteListRenderer.renderList();
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,21 @@ export default class BookTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar");
|
||||
this.$helpNoChildren.toggle(this.shouldDisplayNoChildrenWarning());
|
||||
}
|
||||
|
||||
shouldDisplayNoChildrenWarning() {
|
||||
if (this.note?.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.note?.getAttributeValue("label", "viewType")) {
|
||||
case "calendar":
|
||||
case "table":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
@ -109,24 +109,22 @@ const CALENDAR_VIEWS = [
|
||||
"listMonth"
|
||||
]
|
||||
|
||||
export default class CalendarView extends ViewMode {
|
||||
export default class CalendarView extends ViewMode<{}> {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
private $calendarContainer: JQuery<HTMLElement>;
|
||||
private noteIds: string[];
|
||||
private parentNote: FNote;
|
||||
private calendar?: Calendar;
|
||||
private isCalendarRoot: boolean;
|
||||
private lastView?: string;
|
||||
private debouncedSaveView?: DebouncedFunction<() => void>;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args);
|
||||
super(args, "calendar");
|
||||
|
||||
this.$root = $(TPL);
|
||||
this.$calendarContainer = this.$root.find(".calendar-container");
|
||||
this.noteIds = args.noteIds;
|
||||
this.parentNote = args.parentNote;
|
||||
this.isCalendarRoot = false;
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import treeService from "../../services/tree.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-list">
|
||||
@ -157,26 +158,22 @@ const TPL = /*html*/`
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class ListOrGridView extends ViewMode {
|
||||
class ListOrGridView extends ViewMode<{}> {
|
||||
private $noteList: JQuery<HTMLElement>;
|
||||
|
||||
private parentNote: FNote;
|
||||
private noteIds: string[];
|
||||
private page?: number;
|
||||
private pageSize?: number;
|
||||
private viewType?: string | null;
|
||||
private showNotePath?: boolean;
|
||||
private highlightRegex?: RegExp | null;
|
||||
|
||||
/*
|
||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||
*/
|
||||
constructor(viewType: string, args: ViewModeArgs) {
|
||||
super(args);
|
||||
constructor(viewType: ViewTypeOptions, args: ViewModeArgs) {
|
||||
super(args, viewType);
|
||||
this.$noteList = $(TPL);
|
||||
this.viewType = viewType;
|
||||
|
||||
this.parentNote = args.parentNote;
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
|
||||
this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
||||
|
110
apps/client/src/widgets/view_widgets/table_view/columns.ts
Normal file
110
apps/client/src/widgets/view_widgets/table_view/columns.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { RelationEditor } from "./relation_editor.js";
|
||||
import { NoteFormatter, NoteTitleFormatter } from "./formatters.js";
|
||||
import { applyHeaderMenu } from "./header-menu.js";
|
||||
import type { ColumnDefinition } from "tabulator-tables";
|
||||
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
||||
|
||||
type ColumnType = LabelType | "relation";
|
||||
|
||||
export interface PromotedAttributeInformation {
|
||||
name: string;
|
||||
title?: string;
|
||||
type?: ColumnType;
|
||||
}
|
||||
|
||||
const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
|
||||
text: {
|
||||
editor: "input"
|
||||
},
|
||||
boolean: {
|
||||
formatter: "tickCross",
|
||||
editor: "tickCross"
|
||||
},
|
||||
date: {
|
||||
editor: "date",
|
||||
},
|
||||
datetime: {
|
||||
editor: "datetime"
|
||||
},
|
||||
number: {
|
||||
editor: "number"
|
||||
},
|
||||
time: {
|
||||
editor: "input"
|
||||
},
|
||||
url: {
|
||||
formatter: "link",
|
||||
editor: "input"
|
||||
},
|
||||
relation: {
|
||||
editor: RelationEditor,
|
||||
formatter: NoteFormatter
|
||||
}
|
||||
};
|
||||
|
||||
export function buildColumnDefinitions(info: PromotedAttributeInformation[], existingColumnData?: ColumnDefinition[]) {
|
||||
const columnDefs: ColumnDefinition[] = [
|
||||
{
|
||||
title: "#",
|
||||
formatter: "rownum",
|
||||
headerSort: false,
|
||||
hozAlign: "center",
|
||||
resizable: false,
|
||||
frozen: true
|
||||
},
|
||||
{
|
||||
field: "noteId",
|
||||
title: "Note ID",
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
title: "Title",
|
||||
editor: "input",
|
||||
formatter: NoteTitleFormatter,
|
||||
width: 400
|
||||
}
|
||||
];
|
||||
|
||||
const seenFields = new Set<string>();
|
||||
for (const { name, title, type } of info) {
|
||||
const prefix = (type === "relation" ? "relations" : "labels");
|
||||
const field = `${prefix}.${name}`;
|
||||
|
||||
if (seenFields.has(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
columnDefs.push({
|
||||
field,
|
||||
title: title ?? name,
|
||||
editor: "input",
|
||||
...labelTypeMappings[type ?? "text"],
|
||||
});
|
||||
seenFields.add(field);
|
||||
}
|
||||
|
||||
applyHeaderMenu(columnDefs);
|
||||
if (existingColumnData) {
|
||||
restoreExistingData(columnDefs, existingColumnData);
|
||||
}
|
||||
|
||||
return columnDefs;
|
||||
}
|
||||
|
||||
function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[]) {
|
||||
const byField = new Map<string, ColumnDefinition>;
|
||||
for (const def of oldDefs) {
|
||||
byField.set(def.field ?? "", def);
|
||||
}
|
||||
|
||||
for (const newDef of newDefs) {
|
||||
const oldDef = byField.get(newDef.field ?? "");
|
||||
if (!oldDef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newDef.width = oldDef.width;
|
||||
newDef.visible = oldDef.visible;
|
||||
}
|
||||
}
|
25
apps/client/src/widgets/view_widgets/table_view/dragging.ts
Normal file
25
apps/client/src/widgets/view_widgets/table_view/dragging.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Tabulator } from "tabulator-tables";
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
import branches from "../../../services/branches.js";
|
||||
|
||||
export function canReorderRows(parentNote: FNote) {
|
||||
return !parentNote.hasLabel("sorted")
|
||||
&& parentNote.type !== "search";
|
||||
}
|
||||
|
||||
export function configureReorderingRows(tabulator: Tabulator) {
|
||||
tabulator.on("rowMoved", (row) => {
|
||||
const branchIdsToMove = [ row.getData().branchId ];
|
||||
|
||||
const prevRow = row.getPrevRow();
|
||||
if (prevRow) {
|
||||
branches.moveAfterBranch(branchIdsToMove, prevRow.getData().branchId);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRow = row.getNextRow();
|
||||
if (nextRow) {
|
||||
branches.moveBeforeBranch(branchIdsToMove, nextRow.getData().branchId);
|
||||
}
|
||||
});
|
||||
}
|
22
apps/client/src/widgets/view_widgets/table_view/footer.ts
Normal file
22
apps/client/src/widgets/view_widgets/table_view/footer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
|
||||
function shouldDisplayFooter(parentNote: FNote) {
|
||||
return (parentNote.type !== "search");
|
||||
}
|
||||
|
||||
export default function buildFooter(parentNote: FNote) {
|
||||
if (!shouldDisplayFooter(parentNote)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return /*html*/`\
|
||||
<button class="btn btn-sm" style="padding: 0px 10px 0px 10px;" data-trigger-command="addNewRow">
|
||||
<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">
|
||||
<span class="bx bx-columns"></span> ${t("table_view.new-column")}
|
||||
</button>
|
||||
`.trimStart();
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { CellComponent } from "tabulator-tables";
|
||||
import { loadReferenceLinkTitle } from "../../../services/link.js";
|
||||
|
||||
/**
|
||||
* Custom formatter to represent a note, with the icon and note title being rendered.
|
||||
*
|
||||
* The value of the cell must be the note ID.
|
||||
*/
|
||||
export function NoteFormatter(cell: CellComponent, _formatterParams, onRendered) {
|
||||
let noteId = cell.getValue();
|
||||
if (!noteId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
onRendered(async () => {
|
||||
const { $noteRef, href } = buildNoteLink(noteId);
|
||||
await loadReferenceLinkTitle($noteRef, href);
|
||||
cell.getElement().appendChild($noteRef[0]);
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom formatter for the note title that is quite similar to {@link NoteFormatter}, but where the title and icons are read from separate fields.
|
||||
*/
|
||||
export function NoteTitleFormatter(cell: CellComponent) {
|
||||
const { noteId, iconClass } = cell.getRow().getData();
|
||||
if (!noteId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const { $noteRef } = buildNoteLink(noteId);
|
||||
$noteRef.text(cell.getValue());
|
||||
$noteRef.prepend($("<span>").addClass(iconClass));
|
||||
|
||||
return $noteRef[0].outerHTML;
|
||||
}
|
||||
|
||||
function buildNoteLink(noteId: string) {
|
||||
const $noteRef = $("<span>");
|
||||
const href = `#root/${noteId}`;
|
||||
$noteRef.addClass("reference-link");
|
||||
$noteRef.attr("data-href", href);
|
||||
return { $noteRef, href };
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
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;
|
||||
};
|
265
apps/client/src/widgets/view_widgets/table_view/index.ts
Normal file
265
apps/client/src/widgets/view_widgets/table_view/index.ts
Normal file
@ -0,0 +1,265 @@
|
||||
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 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 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_bootstrap5.min.css";
|
||||
import { canReorderRows, configureReorderingRows } from "./dragging.js";
|
||||
import buildFooter from "./footer.js";
|
||||
import getPromotedAttributeInformation, { buildRowDefinitions } from "./rows.js";
|
||||
import { buildColumnDefinitions } from "./columns.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="table-view">
|
||||
<style>
|
||||
.table-view {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
}
|
||||
|
||||
.table-view-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-result-widget-content .table-view {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tabulator-cell .autocomplete {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: transparent;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header {
|
||||
border-top: unset;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
background-color: unset;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-footer-contents {
|
||||
justify-content: left;
|
||||
gap: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="table-view-container"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export interface StateInfo {
|
||||
tableData?: {
|
||||
columns?: ColumnDefinition[];
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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]);
|
||||
return this.$root;
|
||||
}
|
||||
|
||||
private async renderTable(el: HTMLElement) {
|
||||
const modules = [SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, MenuModule];
|
||||
for (const module of modules) {
|
||||
Tabulator.registerModule(module);
|
||||
}
|
||||
|
||||
this.initialize(el);
|
||||
}
|
||||
|
||||
private async initialize(el: HTMLElement) {
|
||||
const notes = await froca.getNotes(this.args.noteIds);
|
||||
const info = getPromotedAttributeInformation(this.parentNote);
|
||||
|
||||
const viewStorage = await this.viewStorage.restore();
|
||||
this.persistentData = viewStorage?.tableData || {};
|
||||
|
||||
const columnDefs = buildColumnDefinitions(info);
|
||||
const movableRows = canReorderRows(this.parentNote);
|
||||
|
||||
this.api = new Tabulator(el, {
|
||||
layout: "fitDataFill",
|
||||
index: "noteId",
|
||||
columns: columnDefs,
|
||||
data: await buildRowDefinitions(this.parentNote, notes, info),
|
||||
persistence: true,
|
||||
movableColumns: true,
|
||||
movableRows,
|
||||
footerElement: buildFooter(this.parentNote),
|
||||
persistenceWriterFunc: (_id, type: string, data: object) => {
|
||||
(this.persistentData as Record<string, {}>)[type] = data;
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
},
|
||||
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
|
||||
});
|
||||
configureReorderingRows(this.api);
|
||||
this.setupEditing();
|
||||
}
|
||||
|
||||
private onSave() {
|
||||
this.viewStorage.store({
|
||||
tableData: this.persistentData,
|
||||
});
|
||||
}
|
||||
|
||||
private setupEditing() {
|
||||
this.api!.on("cellEdited", async (cell) => {
|
||||
const noteId = cell.getRow().getData().noteId;
|
||||
const field = cell.getField();
|
||||
const newValue = cell.getValue();
|
||||
|
||||
if (field === "title") {
|
||||
server.put(`notes/${noteId}/title`, { title: newValue });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.includes(".")) {
|
||||
const [ type, name ] = field.split(".", 2);
|
||||
if (type === "labels") {
|
||||
setLabel(noteId, name, newValue);
|
||||
} else if (type === "relations") {
|
||||
const note = await froca.getNote(noteId);
|
||||
if (note) {
|
||||
setAttribute(note, "relation", name, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async reloadAttributesCommand() {
|
||||
console.log("Reload attributes");
|
||||
}
|
||||
|
||||
async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) {
|
||||
this.newAttribute = attributes[0];
|
||||
}
|
||||
|
||||
async saveAttributesCommand() {
|
||||
if (!this.newAttribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, value } = this.newAttribute;
|
||||
attributes.addLabel(this.parentNote.noteId, name, value, true);
|
||||
console.log("Save attributes", this.newAttribute);
|
||||
}
|
||||
|
||||
addNewRowCommand() {
|
||||
const parentNotePath = this.args.parentNotePath;
|
||||
if (parentNotePath) {
|
||||
note_create.createNote(parentNotePath, {
|
||||
activate: false
|
||||
}).then(({ note }) => {
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
this.noteIdToEdit = note.noteId;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">): boolean | void {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh if promoted attributes get changed.
|
||||
if (loadResults.getAttributeRows().find(attr =>
|
||||
attr.type === "label" &&
|
||||
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
|
||||
attributes.isAffecting(attr, this.parentNote))) {
|
||||
this.#manageColumnUpdate();
|
||||
}
|
||||
|
||||
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId)) {
|
||||
this.#manageRowsUpdate();
|
||||
}
|
||||
|
||||
if (loadResults.getAttributeRows().some(attr => this.args.noteIds.includes(attr.noteId!))) {
|
||||
this.#manageRowsUpdate();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#manageColumnUpdate() {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getPromotedAttributeInformation(this.parentNote);
|
||||
const columnDefs = buildColumnDefinitions(info, this.persistentData?.columns);
|
||||
this.api.setColumns(columnDefs);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (this.noteIdToEdit) {
|
||||
const row = this.api?.getRows().find(r => r.getData().noteId === this.noteIdToEdit);
|
||||
if (row) {
|
||||
row.getCell("title").edit();
|
||||
}
|
||||
this.noteIdToEdit = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { CellComponent } from "tabulator-tables";
|
||||
import note_autocomplete from "../../../services/note_autocomplete";
|
||||
import froca from "../../../services/froca";
|
||||
|
||||
export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){
|
||||
//cell - the cell component for the editable cell
|
||||
//onRendered - function to call when the editor has been rendered
|
||||
//success - function to call to pass thesuccessfully updated value to Tabulator
|
||||
//cancel - function to call to abort the edit and return to a normal cell
|
||||
//editorParams - params object passed into the editorParams column definition property
|
||||
|
||||
//create and style editor
|
||||
const editor = document.createElement("input");
|
||||
|
||||
const $editor = $(editor);
|
||||
editor.classList.add("form-control");
|
||||
|
||||
//create and style input
|
||||
editor.style.padding = "3px";
|
||||
editor.style.width = "100%";
|
||||
editor.style.boxSizing = "border-box";
|
||||
|
||||
//Set value of editor to the current value of the cell
|
||||
const noteId = cell.getValue();
|
||||
if (noteId) {
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
editor.value = note.title;
|
||||
}
|
||||
|
||||
//set focus on the select box when the editor is selected
|
||||
onRendered(function(){
|
||||
note_autocomplete.initNoteAutocomplete($editor, {
|
||||
allowCreatingNotes: true
|
||||
}).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||
const notePath = suggestion.notePath;
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
success(noteId);
|
||||
});
|
||||
editor.focus();
|
||||
});
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("input-group");
|
||||
container.classList.add("autocomplete");
|
||||
container.appendChild(editor);
|
||||
return container;
|
||||
};
|
74
apps/client/src/widgets/view_widgets/table_view/rows.ts
Normal file
74
apps/client/src/widgets/view_widgets/table_view/rows.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
||||
import type { PromotedAttributeInformation } from "./columns.js";
|
||||
|
||||
export type TableData = {
|
||||
iconClass: string;
|
||||
noteId: string;
|
||||
title: string;
|
||||
labels: Record<string, boolean | string | null>;
|
||||
relations: Record<string, boolean | string | null>;
|
||||
branchId: string;
|
||||
};
|
||||
|
||||
export async function buildRowDefinitions(parentNote: FNote, notes: FNote[], infos: PromotedAttributeInformation[]) {
|
||||
const definitions: TableData[] = [];
|
||||
for (const branch of parentNote.getChildBranches()) {
|
||||
const note = await branch.getNote();
|
||||
if (!note) {
|
||||
continue; // Skip if the note is not found
|
||||
}
|
||||
|
||||
const labels: typeof definitions[0]["labels"] = {};
|
||||
const relations: typeof definitions[0]["relations"] = {};
|
||||
for (const { name, type } of infos) {
|
||||
if (type === "relation") {
|
||||
relations[name] = note.getRelationValue(name);
|
||||
} else if (type === "boolean") {
|
||||
labels[name] = note.hasLabel(name);
|
||||
} else {
|
||||
labels[name] = note.getLabelValue(name);
|
||||
}
|
||||
}
|
||||
definitions.push({
|
||||
iconClass: note.getIcon(),
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
labels,
|
||||
relations,
|
||||
branchId: branch.branchId
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
export default function getPromotedAttributeInformation(parentNote: FNote) {
|
||||
const info: PromotedAttributeInformation[] = [];
|
||||
for (const promotedAttribute of parentNote.getPromotedDefinitionAttributes()) {
|
||||
const def = promotedAttribute.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") {
|
||||
console.warn("Relations are not supported for now");
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: LabelType | "relation" = def.labelType || "text";
|
||||
if (labelType === "relation") {
|
||||
type = "relation";
|
||||
}
|
||||
|
||||
info.push({
|
||||
name,
|
||||
title: def.promotedAlias,
|
||||
type
|
||||
});
|
||||
}
|
||||
console.log("Promoted attribute information", info);
|
||||
return info;
|
||||
}
|
@ -1,18 +1,30 @@
|
||||
import type { EventData } 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";
|
||||
import ViewModeStorage from "./view_mode_storage.js";
|
||||
|
||||
export interface ViewModeArgs {
|
||||
$parent: JQuery<HTMLElement>;
|
||||
parentNote: FNote;
|
||||
parentNotePath?: string | null;
|
||||
noteIds: string[];
|
||||
showNotePath?: boolean;
|
||||
}
|
||||
|
||||
export default abstract class ViewMode {
|
||||
export default abstract class ViewMode<T extends object> extends Component {
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
private _viewStorage: ViewModeStorage<T> | null;
|
||||
protected parentNote: FNote;
|
||||
protected viewType: ViewTypeOptions;
|
||||
|
||||
constructor(args: ViewModeArgs, viewType: ViewTypeOptions) {
|
||||
super();
|
||||
this.parentNote = args.parentNote;
|
||||
this._viewStorage = null;
|
||||
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
|
||||
args.$parent.empty();
|
||||
this.viewType = viewType;
|
||||
}
|
||||
|
||||
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
||||
@ -32,4 +44,13 @@ export default abstract class ViewMode {
|
||||
return false;
|
||||
}
|
||||
|
||||
get viewStorage() {
|
||||
if (this._viewStorage) {
|
||||
return this._viewStorage;
|
||||
}
|
||||
|
||||
this._viewStorage = new ViewModeStorage(this.parentNote, this.viewType);
|
||||
return this._viewStorage;
|
||||
}
|
||||
|
||||
}
|
||||
|
43
apps/client/src/widgets/view_widgets/view_mode_storage.ts
Normal file
43
apps/client/src/widgets/view_widgets/view_mode_storage.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type FNote from "../../entities/fnote";
|
||||
import type { ViewTypeOptions } from "../../services/note_list_renderer";
|
||||
import server from "../../services/server";
|
||||
|
||||
const ATTACHMENT_ROLE = "viewConfig";
|
||||
|
||||
export default class ViewModeStorage<T extends object> {
|
||||
|
||||
private note: FNote;
|
||||
private attachmentName: string;
|
||||
|
||||
constructor(note: FNote, viewType: ViewTypeOptions) {
|
||||
this.note = note;
|
||||
this.attachmentName = viewType + ".json";
|
||||
}
|
||||
|
||||
async store(data: T) {
|
||||
const payload = {
|
||||
role: ATTACHMENT_ROLE,
|
||||
title: this.attachmentName,
|
||||
mime: "application/json",
|
||||
content: JSON.stringify(data),
|
||||
position: 0
|
||||
};
|
||||
await server.post(`notes/${this.note.noteId}/attachments?matchBy=title`, payload);
|
||||
}
|
||||
|
||||
async restore() {
|
||||
const existingAttachments = await this.note.getAttachmentsByRole(ATTACHMENT_ROLE);
|
||||
if (existingAttachments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachment = existingAttachments
|
||||
.find(a => a.title === this.attachmentName);
|
||||
if (!attachment) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachmentData = await server.get<{ content: string } | null>(`attachments/${attachment.attachmentId}/blob`);
|
||||
return JSON.parse(attachmentData?.content ?? "{}");
|
||||
}
|
||||
}
|
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
98
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table.html
generated
vendored
Normal file
98
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table.html
generated
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:1050/259;" src="Table_image.png" width="1050"
|
||||
height="259">
|
||||
</figure>
|
||||
<p>The table view displays information in a grid, where the rows are individual
|
||||
notes and the columns are <a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
|
||||
In addition, values are editable.</p>
|
||||
<h2>Interaction</h2>
|
||||
<h3>Creating a new table</h3>
|
||||
<p>Right click the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Insert child note</em> and look for the <em>Table item</em>.</p>
|
||||
<h3>Adding columns</h3>
|
||||
<p>Each column is a <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">promoted attribute</a> that
|
||||
is defined on the Book note. Ideally, the promoted attributes need to be
|
||||
inheritable in order to show up in the child notes.</p>
|
||||
<p>To create a new column, simply press <em>Add new column </em>at the bottom
|
||||
of the table.</p>
|
||||
<p>There are also a few predefined columns:</p>
|
||||
<ul>
|
||||
<li>The current item number, identified by the <code>#</code> symbol. This simply
|
||||
counts the note and is affected by sorting.</li>
|
||||
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_m1lbrzyKDaRB">Note ID</a>,
|
||||
representing the unique ID used internally by Trilium</li>
|
||||
<li>The title of the note.</li>
|
||||
</ul>
|
||||
<h3>Adding new rows</h3>
|
||||
<p>Each row is actually a note that is a child of the book note.</p>
|
||||
<p>To create a new note, press <em>Add new row</em> at the bottom of the table.
|
||||
By default it will try to edit the title of the newly created note.</p>
|
||||
<p>Alternatively, the note can be created from the<a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> or
|
||||
<a
|
||||
href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">scripting</a>.</p>
|
||||
<h3>Editing data</h3>
|
||||
<p>Simply click on a cell within a row to change its value. The change will
|
||||
not only reflect in the table, but also as an attribute of the corresponding
|
||||
note.</p>
|
||||
<ul>
|
||||
<li>The editing will respect the type of the promoted attribute, by presenting
|
||||
a normal text box, a number selector or a date selector for example.</li>
|
||||
<li>It also possible to change the title of a note.</li>
|
||||
<li>Editing relations is also possible, by using the note autocomplete.</li>
|
||||
</ul>
|
||||
<h2>Working with the data</h2>
|
||||
<h3>Sorting</h3>
|
||||
<p>It is possible to sort the data by the values of a column:</p>
|
||||
<ul>
|
||||
<li>To do so, simply click on a column.</li>
|
||||
<li>To switch between ascending or descending sort, simply click again on
|
||||
the same column. The arrow next to the column will indicate the direction
|
||||
of the sort.</li>
|
||||
</ul>
|
||||
<h3>Reordering and hiding columns</h3>
|
||||
<ul>
|
||||
<li>Columns can be reordered by dragging the header of the columns.</li>
|
||||
<li>Columns can be hidden or shown by right clicking on a column and clicking
|
||||
the item corresponding to the column.</li>
|
||||
</ul>
|
||||
<h3>Reordering rows</h3>
|
||||
<p>Notes can be dragged around to change their order. This will also change
|
||||
the order of the note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</p>
|
||||
<p>Currently, it's possible to reorder notes even if sorting is used, but
|
||||
the result might be inconsistent.</p>
|
||||
<h2>Limitations</h2>
|
||||
<p>The table functionality is still in its early stages, as such it faces
|
||||
quite a few important limitations:</p>
|
||||
<ol>
|
||||
<li>As mentioned previously, the columns of the table are defined as
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.
|
||||
<ol>
|
||||
<li>But only the promoted attributes that are defined at the level of the
|
||||
Book note are actually taken into consideration.</li>
|
||||
<li>There are plans to recursively look for columns across the sub-hierarchy.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Hierarchy is not yet supported, so the table will only show the items
|
||||
that are direct children of the <em>Book</em> note.</li>
|
||||
<li>Multiple labels and relations are not supported. If a <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a> is
|
||||
defined with a <em>Multi value</em> specificity, they will be ignored.</li>
|
||||
</ol>
|
||||
<h2>Use in search</h2>
|
||||
<p>The table view can be used in a <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a> by
|
||||
adding the <code>#viewType=table</code> attribute.</p>
|
||||
<p>Unlike when used in a book, saved searches are not limited to the sub-hierarchy
|
||||
of a note and allows for advanced queries thanks to the power of the
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</p>
|
||||
<p>However, there are also some limitations:</p>
|
||||
<ul>
|
||||
<li>It's not possible to reorder notes.</li>
|
||||
<li>It's not possible to add a new row.</li>
|
||||
</ul>
|
||||
<p>Columns are supported, by being defined as <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a> to
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a> note.</p>
|
||||
<p>Editing is also supported.</p>
|
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table_image.png
generated
vendored
Normal file
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table_image.png
generated
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
3
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Note Map.html
generated
vendored
3
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Note Map.html
generated
vendored
@ -7,3 +7,6 @@
|
||||
<p>Once created, the note map will display the relations between notes. Only
|
||||
the notes that are part of the parent of the note map will be displayed
|
||||
(including their children).</p>
|
||||
<p>The labels <code>mapIncludeRelation</code> and <code>mapExcludeRelation</code>,
|
||||
if set, filter the note map to include only the specified relations or
|
||||
to exclude the specified relations, respectively.</p>
|
@ -48,7 +48,8 @@ function updateNoteAttribute(req: Request) {
|
||||
attribute = new BAttribute({
|
||||
noteId: noteId,
|
||||
name: body.name,
|
||||
type: body.type
|
||||
type: body.type,
|
||||
isInheritable: body.isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,23 @@ export default function buildHiddenSubtreeTemplates() {
|
||||
value: "promoted,alias=Description,single,text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_table",
|
||||
type: "book",
|
||||
title: "Table",
|
||||
icon: "bx bx-table",
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "viewType",
|
||||
type: "label",
|
||||
value: "table"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
2
docs/Developer Guide/!!!meta.json
vendored
2
docs/Developer Guide/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.95.0",
|
||||
"appVersion": "0.96.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
2
docs/Release Notes/!!!meta.json
vendored
2
docs/Release Notes/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.95.0",
|
||||
"appVersion": "0.96.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
3
docs/Release Notes/Release Notes/v0.96.0.md
vendored
3
docs/Release Notes/Release Notes/v0.96.0.md
vendored
@ -1,12 +1,11 @@
|
||||
# v0.96.0
|
||||
|
||||
> [!NOTE]
|
||||
> The Docker image has been relocated to `triliumnext/trilium`. Please update your configuration accordingly.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you enjoyed this release, consider showing a token of appreciation by:
|
||||
>
|
||||
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Notes) (top-right).
|
||||
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
|
||||
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
|
||||
|
||||
## 💡 Key highlights
|
||||
|
82
docs/User Guide/!!!meta.json
vendored
82
docs/User Guide/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.95.0",
|
||||
"appVersion": "0.96.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@ -3420,6 +3420,86 @@
|
||||
"dataFileName": "11_Calendar View_image.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "2FvYrpmOXm29",
|
||||
"notePath": [
|
||||
"pOsGYCXsbNQG",
|
||||
"gh7bpGYxajRS",
|
||||
"BFs8mudNFgCS",
|
||||
"0ESUbbAxVnoK",
|
||||
"2FvYrpmOXm29"
|
||||
],
|
||||
"title": "Table",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
"value": "bx bx-table",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "OFXdgB2nNk1F",
|
||||
"isInheritable": false,
|
||||
"position": 20
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oPVyFC7WL2Lp",
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "m1lbrzyKDaRB",
|
||||
"isInheritable": false,
|
||||
"position": 40
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "CdNpE2pqjmI6",
|
||||
"isInheritable": false,
|
||||
"position": 50
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "m523cpzocqaD",
|
||||
"isInheritable": false,
|
||||
"position": 60
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eIg8jdvaoNNd",
|
||||
"isInheritable": false,
|
||||
"position": 70
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Table.md",
|
||||
"attachments": [
|
||||
{
|
||||
"attachmentId": "vJYUG9fLQ2Pd",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "Table_image.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
83
docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table.md
vendored
Normal file
83
docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table.md
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
# Table
|
||||
<figure class="image"><img style="aspect-ratio:1050/259;" src="Table_image.png" width="1050" height="259"></figure>
|
||||
|
||||
The table view displays information in a grid, where the rows are individual notes and the columns are <a class="reference-link" href="../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>. In addition, values are editable.
|
||||
|
||||
## Interaction
|
||||
|
||||
### Creating a new table
|
||||
|
||||
Right click the <a class="reference-link" href="../../UI%20Elements/Note%20Tree.md">Note Tree</a> and select _Insert child note_ and look for the _Table item_.
|
||||
|
||||
### Adding columns
|
||||
|
||||
Each column is a [promoted attribute](../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md) that is defined on the Book note. Ideally, the promoted attributes need to be inheritable in order to show up in the child notes.
|
||||
|
||||
To create a new column, simply press _Add new column_ at the bottom of the table.
|
||||
|
||||
There are also a few predefined columns:
|
||||
|
||||
* The current item number, identified by the `#` symbol. This simply counts the note and is affected by sorting.
|
||||
* <a class="reference-link" href="../../../Advanced%20Usage/Note%20ID.md">Note ID</a>, representing the unique ID used internally by Trilium
|
||||
* The title of the note.
|
||||
|
||||
### Adding new rows
|
||||
|
||||
Each row is actually a note that is a child of the book note.
|
||||
|
||||
To create a new note, press _Add new row_ at the bottom of the table. By default it will try to edit the title of the newly created note.
|
||||
|
||||
Alternatively, the note can be created from the<a class="reference-link" href="../../UI%20Elements/Note%20Tree.md">Note Tree</a> or [scripting](../../../Scripting.md).
|
||||
|
||||
### Editing data
|
||||
|
||||
Simply click on a cell within a row to change its value. The change will not only reflect in the table, but also as an attribute of the corresponding note.
|
||||
|
||||
* The editing will respect the type of the promoted attribute, by presenting a normal text box, a number selector or a date selector for example.
|
||||
* It also possible to change the title of a note.
|
||||
* Editing relations is also possible, by using the note autocomplete.
|
||||
|
||||
## Working with the data
|
||||
|
||||
### Sorting
|
||||
|
||||
It is possible to sort the data by the values of a column:
|
||||
|
||||
* To do so, simply click on a column.
|
||||
* To switch between ascending or descending sort, simply click again on the same column. The arrow next to the column will indicate the direction of the sort.
|
||||
|
||||
### Reordering and hiding columns
|
||||
|
||||
* Columns can be reordered by dragging the header of the columns.
|
||||
* Columns can be hidden or shown by right clicking on a column and clicking the item corresponding to the column.
|
||||
|
||||
### Reordering rows
|
||||
|
||||
Notes can be dragged around to change their order. This will also change the order of the note in the <a class="reference-link" href="../../UI%20Elements/Note%20Tree.md">Note Tree</a>.
|
||||
|
||||
Currently, it's possible to reorder notes even if sorting is used, but the result might be inconsistent.
|
||||
|
||||
## Limitations
|
||||
|
||||
The table functionality is still in its early stages, as such it faces quite a few important limitations:
|
||||
|
||||
1. As mentioned previously, the columns of the table are defined as <a class="reference-link" href="../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>.
|
||||
1. But only the promoted attributes that are defined at the level of the Book note are actually taken into consideration.
|
||||
2. There are plans to recursively look for columns across the sub-hierarchy.
|
||||
2. Hierarchy is not yet supported, so the table will only show the items that are direct children of the _Book_ note.
|
||||
3. Multiple labels and relations are not supported. If a <a class="reference-link" href="../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a> is defined with a _Multi value_ specificity, they will be ignored.
|
||||
|
||||
## Use in search
|
||||
|
||||
The table view can be used in a <a class="reference-link" href="../../../Note%20Types/Saved%20Search.md">Saved Search</a> by adding the `#viewType=table` attribute.
|
||||
|
||||
Unlike when used in a book, saved searches are not limited to the sub-hierarchy of a note and allows for advanced queries thanks to the power of the <a class="reference-link" href="../../Navigation/Search.md">Search</a>.
|
||||
|
||||
However, there are also some limitations:
|
||||
|
||||
* It's not possible to reorder notes.
|
||||
* It's not possible to add a new row.
|
||||
|
||||
Columns are supported, by being defined as <a class="reference-link" href="../../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a> to the <a class="reference-link" href="../../../Note%20Types/Saved%20Search.md">Saved Search</a> note.
|
||||
|
||||
Editing is also supported.
|
BIN
docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table_image.png
vendored
Normal file
BIN
docs/User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Table_image.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -286,6 +286,9 @@ importers:
|
||||
svg-pan-zoom:
|
||||
specifier: 3.6.2
|
||||
version: 3.6.2
|
||||
tabulator-tables:
|
||||
specifier: 6.3.1
|
||||
version: 6.3.1
|
||||
vanilla-js-wheel-zoom:
|
||||
specifier: 9.0.4
|
||||
version: 9.0.4
|
||||
@ -308,6 +311,9 @@ importers:
|
||||
'@types/mark.js':
|
||||
specifier: 8.11.12
|
||||
version: 8.11.12
|
||||
'@types/tabulator-tables':
|
||||
specifier: 6.2.6
|
||||
version: 6.2.6
|
||||
copy-webpack-plugin:
|
||||
specifier: 13.0.0
|
||||
version: 13.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.5))
|
||||
@ -5606,6 +5612,9 @@ packages:
|
||||
'@types/swagger-ui@5.21.1':
|
||||
resolution: {integrity: sha512-DUmUH59eeOtvAqcWwBduH2ws0cc5i95KHsXCS4FsOfbUq/clW8TN+HqRBj7q5p9MSsSNK43RziIGItNbrAGLxg==}
|
||||
|
||||
'@types/tabulator-tables@6.2.6':
|
||||
resolution: {integrity: sha512-A+2VrqDluI6hNw5dQl1Z7b8pjQfAE62+3Kj0cFfenWzj0T0ewMicPrpPINHL7ASqz9u9FTDn1Mz1Ige2tF4Wlw==}
|
||||
|
||||
'@types/tmp@0.2.6':
|
||||
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
||||
|
||||
@ -12987,6 +12996,9 @@ packages:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
tabulator-tables@6.3.1:
|
||||
resolution: {integrity: sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==}
|
||||
|
||||
tailwindcss@4.1.11:
|
||||
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
||||
|
||||
@ -20461,6 +20473,8 @@ snapshots:
|
||||
|
||||
'@types/swagger-ui@5.21.1': {}
|
||||
|
||||
'@types/tabulator-tables@6.2.6': {}
|
||||
|
||||
'@types/tmp@0.2.6': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
@ -29295,6 +29309,8 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
tabulator-tables@6.3.1: {}
|
||||
|
||||
tailwindcss@4.1.11: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user