mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 23:29:02 +02:00
Introduce the table view (#6097)
This commit is contained in:
commit
ac8b0535d2
@ -54,6 +54,7 @@
|
|||||||
"preact": "10.26.9",
|
"preact": "10.26.9",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
|
"tabulator-tables": "6.3.1",
|
||||||
"vanilla-js-wheel-zoom": "9.0.4"
|
"vanilla-js-wheel-zoom": "9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -63,6 +64,7 @@
|
|||||||
"@types/leaflet": "1.9.19",
|
"@types/leaflet": "1.9.19",
|
||||||
"@types/leaflet-gpx": "1.3.7",
|
"@types/leaflet-gpx": "1.3.7",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
|
"@types/tabulator-tables": "6.2.6",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"happy-dom": "18.0.1",
|
"happy-dom": "18.0.1",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
|
@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
if (fun) {
|
if (fun) {
|
||||||
return this.callMethod(fun, data);
|
return this.callMethod(fun, data);
|
||||||
} else {
|
} else if (this.parent) {
|
||||||
if (!this.parent) {
|
|
||||||
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parent.triggerCommand(name, data);
|
return this.parent.triggerCommand(name, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,14 +315,38 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasNoteList() {
|
hasNoteList() {
|
||||||
return (
|
const note = this.note;
|
||||||
this.note &&
|
|
||||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
if (!note) {
|
||||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
return false;
|
||||||
["book", "text", "code"].includes(this.note.type) &&
|
}
|
||||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
|
||||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
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) {
|
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||||
|
@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js";
|
|||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
orientation?: "left";
|
orientation?: "left";
|
||||||
@ -28,6 +28,7 @@ export interface MenuCommandItem<T> {
|
|||||||
items?: MenuItem<T>[] | null;
|
items?: MenuItem<T>[] | null;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
spellingSuggestion?: string;
|
spellingSuggestion?: string;
|
||||||
|
checked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||||
@ -146,11 +147,14 @@ class ContextMenu {
|
|||||||
} else {
|
} else {
|
||||||
const $icon = $("<span>");
|
const $icon = $("<span>");
|
||||||
|
|
||||||
if ("uiIcon" in item && item.uiIcon) {
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
$icon.addClass(item.uiIcon);
|
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||||
|
if (icon) {
|
||||||
|
$icon.addClass(icon);
|
||||||
} else {
|
} else {
|
||||||
$icon.append(" ");
|
$icon.append(" ");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const $link = $("<span>")
|
const $link = $("<span>")
|
||||||
.append($icon)
|
.append($icon)
|
||||||
|
@ -3,15 +3,16 @@ import froca from "./froca.js";
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { AttributeRow } from "./load_results.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`, {
|
await server.put(`notes/${noteId}/attribute`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name: name,
|
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`, {
|
await server.put(`notes/${noteId}/set-attribute`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name: name,
|
name: name,
|
||||||
@ -49,7 +50,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
|||||||
* @param name the name of the attribute to set.
|
* @param name the name of the attribute to set.
|
||||||
* @param value the value 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) {
|
if (value) {
|
||||||
// Create or update the attribute.
|
// Create or update the attribute.
|
||||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
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>) {
|
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||||
const blob = await note.getBlob();
|
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>");
|
const $codeBlock = $("<code>");
|
||||||
$codeBlock.text(blob?.content || "");
|
$codeBlock.text(content);
|
||||||
$renderedContent.append($("<pre>").append($codeBlock));
|
$renderedContent.append($("<pre>").append($codeBlock));
|
||||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||||
}
|
}
|
||||||
@ -301,7 +310,7 @@ function getRenderingType(entity: FNote | FAttachment) {
|
|||||||
|
|
||||||
if (type === "file" && mime === "application/pdf") {
|
if (type === "file" && mime === "application/pdf") {
|
||||||
type = "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";
|
type = "code";
|
||||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||||
type = "audio";
|
type = "audio";
|
||||||
|
@ -384,7 +384,7 @@ function linkContextMenu(e: PointerEvent) {
|
|||||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
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");
|
const $link = $el[0].tagName === "A" ? $el : $el.find("a");
|
||||||
|
|
||||||
href = href || $link.attr("href");
|
href = href || $link.attr("href");
|
||||||
|
@ -1,30 +1,32 @@
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_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 { 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 ViewTypeOptions = "list" | "grid" | "calendar";
|
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table";
|
||||||
|
|
||||||
export default class NoteListRenderer {
|
export default class NoteListRenderer {
|
||||||
|
|
||||||
private viewType: ViewTypeOptions;
|
private viewType: ViewTypeOptions;
|
||||||
public viewMode: ViewMode | null;
|
public viewMode: ViewMode<any> | null;
|
||||||
|
|
||||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
|
constructor(args: ViewModeArgs) {
|
||||||
this.viewType = this.#getViewType(parentNote);
|
this.viewType = this.#getViewType(args.parentNote);
|
||||||
const args: ViewModeArgs = {
|
|
||||||
$parent,
|
|
||||||
parentNote,
|
|
||||||
noteIds,
|
|
||||||
showNotePath
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.viewType === "list" || this.viewType === "grid") {
|
switch (this.viewType) {
|
||||||
|
case "list":
|
||||||
|
case "grid":
|
||||||
this.viewMode = new ListOrGridView(this.viewType, args);
|
this.viewMode = new ListOrGridView(this.viewType, args);
|
||||||
} else if (this.viewType === "calendar") {
|
break;
|
||||||
|
case "calendar":
|
||||||
this.viewMode = new CalendarView(args);
|
this.viewMode = new CalendarView(args);
|
||||||
} else {
|
break;
|
||||||
|
case "table":
|
||||||
|
this.viewMode = new TableView(args);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
this.viewMode = null;
|
this.viewMode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +34,7 @@ export default class NoteListRenderer {
|
|||||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||||
const viewType = parentNote.getLabelValue("viewType");
|
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
|
// when not explicitly set, decide based on the note type
|
||||||
return parentNote.type === "search" ? "list" : "grid";
|
return parentNote.type === "search" ? "list" : "grid";
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,6 +14,7 @@ let dismissTimer: ReturnType<typeof setTimeout>;
|
|||||||
|
|
||||||
function setupGlobalTooltip() {
|
function setupGlobalTooltip() {
|
||||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
$(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
|
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
||||||
$(document).on("click", (e) => {
|
$(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";
|
type Multiplicity = "single" | "multi";
|
||||||
|
|
||||||
export interface DefinitionObject {
|
export interface DefinitionObject {
|
||||||
|
@ -760,7 +760,8 @@
|
|||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"book_properties": "Book Properties",
|
"book_properties": "Book Properties",
|
||||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||||
"calendar": "Calendar"
|
"calendar": "Calendar",
|
||||||
|
"table": "Table"
|
||||||
},
|
},
|
||||||
"edited_notes": {
|
"edited_notes": {
|
||||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||||
@ -1933,5 +1934,9 @@
|
|||||||
"title": "Features",
|
"title": "Features",
|
||||||
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
||||||
"note_completion_enabled": "Enable note 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> = {
|
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||||
list: null,
|
list: null,
|
||||||
grid: null,
|
grid: null,
|
||||||
calendar: "xWbu3jpNWapp"
|
calendar: "xWbu3jpNWapp",
|
||||||
|
table: "2FvYrpmOXm29"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
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, 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 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">
|
||||||
@ -36,7 +38,15 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
private isIntersecting?: boolean;
|
private isIntersecting?: boolean;
|
||||||
private noteIdRefreshed?: string;
|
private noteIdRefreshed?: string;
|
||||||
private shownNoteId?: string | null;
|
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() {
|
isEnabled() {
|
||||||
return super.isEnabled() && this.noteContext?.hasNoteList();
|
return super.isEnabled() && this.noteContext?.hasNoteList();
|
||||||
@ -46,6 +56,7 @@ 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) => {
|
||||||
@ -64,6 +75,23 @@ 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);
|
||||||
@ -76,7 +104,12 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderNoteList(note: FNote) {
|
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);
|
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||||
await noteListRenderer.renderList();
|
await noteListRenderer.renderList();
|
||||||
this.viewMode = noteListRenderer.viewMode;
|
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="grid">${t("book_properties.grid")}</option>
|
||||||
<option value="list">${t("book_properties.list")}</option>
|
<option value="list">${t("book_properties.list")}</option>
|
||||||
<option value="calendar">${t("book_properties.calendar")}</option>
|
<option value="calendar">${t("book_properties.calendar")}</option>
|
||||||
|
<option value="table">${t("book_properties.table")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -67,7 +68,6 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
|||||||
getTitle() {
|
getTitle() {
|
||||||
return {
|
return {
|
||||||
show: this.isEnabled(),
|
show: this.isEnabled(),
|
||||||
activate: true,
|
|
||||||
title: t("book_properties.book_properties"),
|
title: t("book_properties.book_properties"),
|
||||||
icon: "bx bx-book"
|
icon: "bx bx-book"
|
||||||
};
|
};
|
||||||
@ -126,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["list", "grid", "calendar"].includes(type)) {
|
if (!["list", "grid", "calendar", "table"].includes(type)) {
|
||||||
throw new Error(t("book_properties.invalid_view_type", { 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
|
// the order of attributes is important as well
|
||||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
ownedAttributes.sort((a, b) => a.position - b.position);
|
||||||
|
|
||||||
if (promotedDefAttrs.length === 0) {
|
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
|
||||||
this.toggleInt(false);
|
this.toggleInt(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,13 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
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();
|
await noteListRenderer.renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,21 @@ export default class BookTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note: FNote) {
|
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">) {
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
@ -109,24 +109,22 @@ const CALENDAR_VIEWS = [
|
|||||||
"listMonth"
|
"listMonth"
|
||||||
]
|
]
|
||||||
|
|
||||||
export default class CalendarView extends ViewMode {
|
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 noteIds: string[];
|
||||||
private parentNote: FNote;
|
|
||||||
private calendar?: Calendar;
|
private calendar?: Calendar;
|
||||||
private isCalendarRoot: boolean;
|
private isCalendarRoot: boolean;
|
||||||
private lastView?: string;
|
private lastView?: string;
|
||||||
private debouncedSaveView?: DebouncedFunction<() => void>;
|
private debouncedSaveView?: DebouncedFunction<() => void>;
|
||||||
|
|
||||||
constructor(args: ViewModeArgs) {
|
constructor(args: ViewModeArgs) {
|
||||||
super(args);
|
super(args, "calendar");
|
||||||
|
|
||||||
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.noteIds = args.noteIds;
|
||||||
this.parentNote = args.parentNote;
|
|
||||||
this.isCalendarRoot = false;
|
this.isCalendarRoot = false;
|
||||||
args.$parent.append(this.$root);
|
args.$parent.append(this.$root);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import treeService from "../../services/tree.js";
|
|||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||||
|
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="note-list">
|
<div class="note-list">
|
||||||
@ -157,26 +158,22 @@ const TPL = /*html*/`
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
class ListOrGridView extends ViewMode {
|
class ListOrGridView extends ViewMode<{}> {
|
||||||
private $noteList: JQuery<HTMLElement>;
|
private $noteList: JQuery<HTMLElement>;
|
||||||
|
|
||||||
private parentNote: FNote;
|
|
||||||
private noteIds: string[];
|
private noteIds: string[];
|
||||||
private page?: number;
|
private page?: number;
|
||||||
private pageSize?: number;
|
private pageSize?: number;
|
||||||
private viewType?: string | null;
|
|
||||||
private showNotePath?: boolean;
|
private showNotePath?: boolean;
|
||||||
private highlightRegex?: RegExp | null;
|
private highlightRegex?: RegExp | null;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||||
*/
|
*/
|
||||||
constructor(viewType: string, args: ViewModeArgs) {
|
constructor(viewType: ViewTypeOptions, args: ViewModeArgs) {
|
||||||
super(args);
|
super(args, viewType);
|
||||||
this.$noteList = $(TPL);
|
this.$noteList = $(TPL);
|
||||||
this.viewType = viewType;
|
|
||||||
|
|
||||||
this.parentNote = args.parentNote;
|
|
||||||
const includedNoteIds = this.getIncludedNoteIds();
|
const includedNoteIds = this.getIncludedNoteIds();
|
||||||
|
|
||||||
this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
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 type { EventData } from "../../components/app_context.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 ViewModeStorage from "./view_mode_storage.js";
|
||||||
|
|
||||||
export interface ViewModeArgs {
|
export interface ViewModeArgs {
|
||||||
$parent: JQuery<HTMLElement>;
|
$parent: JQuery<HTMLElement>;
|
||||||
parentNote: FNote;
|
parentNote: FNote;
|
||||||
|
parentNotePath?: string | null;
|
||||||
noteIds: string[];
|
noteIds: string[];
|
||||||
showNotePath?: boolean;
|
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
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
||||||
@ -32,4 +44,13 @@ export default abstract class ViewMode {
|
|||||||
return false;
|
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
|
<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
|
the notes that are part of the parent of the note map will be displayed
|
||||||
(including their children).</p>
|
(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({
|
attribute = new BAttribute({
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
name: body.name,
|
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"
|
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,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.95.0",
|
"appVersion": "0.96.0",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"isClone": false,
|
||||||
|
2
docs/Release Notes/!!!meta.json
vendored
2
docs/Release Notes/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.95.0",
|
"appVersion": "0.96.0",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"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
|
# v0.96.0
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The Docker image has been relocated to `triliumnext/trilium`. Please update your configuration accordingly.
|
> The Docker image has been relocated to `triliumnext/trilium`. Please update your configuration accordingly.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> If you enjoyed this release, consider showing a token of appreciation by:
|
> 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).
|
> * 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
|
## 💡 Key highlights
|
||||||
|
82
docs/User Guide/!!!meta.json
vendored
82
docs/User Guide/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.95.0",
|
"appVersion": "0.96.0",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"isClone": false,
|
||||||
@ -3420,6 +3420,86 @@
|
|||||||
"dataFileName": "11_Calendar View_image.png"
|
"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:
|
svg-pan-zoom:
|
||||||
specifier: 3.6.2
|
specifier: 3.6.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
|
tabulator-tables:
|
||||||
|
specifier: 6.3.1
|
||||||
|
version: 6.3.1
|
||||||
vanilla-js-wheel-zoom:
|
vanilla-js-wheel-zoom:
|
||||||
specifier: 9.0.4
|
specifier: 9.0.4
|
||||||
version: 9.0.4
|
version: 9.0.4
|
||||||
@ -308,6 +311,9 @@ importers:
|
|||||||
'@types/mark.js':
|
'@types/mark.js':
|
||||||
specifier: 8.11.12
|
specifier: 8.11.12
|
||||||
version: 8.11.12
|
version: 8.11.12
|
||||||
|
'@types/tabulator-tables':
|
||||||
|
specifier: 6.2.6
|
||||||
|
version: 6.2.6
|
||||||
copy-webpack-plugin:
|
copy-webpack-plugin:
|
||||||
specifier: 13.0.0
|
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))
|
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':
|
'@types/swagger-ui@5.21.1':
|
||||||
resolution: {integrity: sha512-DUmUH59eeOtvAqcWwBduH2ws0cc5i95KHsXCS4FsOfbUq/clW8TN+HqRBj7q5p9MSsSNK43RziIGItNbrAGLxg==}
|
resolution: {integrity: sha512-DUmUH59eeOtvAqcWwBduH2ws0cc5i95KHsXCS4FsOfbUq/clW8TN+HqRBj7q5p9MSsSNK43RziIGItNbrAGLxg==}
|
||||||
|
|
||||||
|
'@types/tabulator-tables@6.2.6':
|
||||||
|
resolution: {integrity: sha512-A+2VrqDluI6hNw5dQl1Z7b8pjQfAE62+3Kj0cFfenWzj0T0ewMicPrpPINHL7ASqz9u9FTDn1Mz1Ige2tF4Wlw==}
|
||||||
|
|
||||||
'@types/tmp@0.2.6':
|
'@types/tmp@0.2.6':
|
||||||
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
||||||
|
|
||||||
@ -12987,6 +12996,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
tabulator-tables@6.3.1:
|
||||||
|
resolution: {integrity: sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==}
|
||||||
|
|
||||||
tailwindcss@4.1.11:
|
tailwindcss@4.1.11:
|
||||||
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
||||||
|
|
||||||
@ -20461,6 +20473,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/swagger-ui@5.21.1': {}
|
'@types/swagger-ui@5.21.1': {}
|
||||||
|
|
||||||
|
'@types/tabulator-tables@6.2.6': {}
|
||||||
|
|
||||||
'@types/tmp@0.2.6': {}
|
'@types/tmp@0.2.6': {}
|
||||||
|
|
||||||
'@types/tough-cookie@4.0.5': {}
|
'@types/tough-cookie@4.0.5': {}
|
||||||
@ -29295,6 +29309,8 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
tabulator-tables@6.3.1: {}
|
||||||
|
|
||||||
tailwindcss@4.1.11: {}
|
tailwindcss@4.1.11: {}
|
||||||
|
|
||||||
tapable@2.2.1: {}
|
tapable@2.2.1: {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user