Merge branch 'main' into feat/llm-tool-improvement

This commit is contained in:
Jon Fuller
2025-07-04 16:49:24 -07:00
committed by GitHub
71 changed files with 3307 additions and 2260 deletions

View File

@@ -120,7 +120,7 @@ jobs:
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
fail_on_unmatched_files: true
files: upload/*.*
discussion_category_name: Announcements
discussion_category_name: Releases
make_latest: ${{ !contains(github.ref, 'rc') }}
prerelease: ${{ contains(github.ref, 'rc') }}
token: ${{ secrets.RELEASE_PAT }}

View File

@@ -35,13 +35,13 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js"
},
"devDependencies": {
"@playwright/test": "1.53.1",
"@stylistic/eslint-plugin": "5.0.0",
"@playwright/test": "1.53.2",
"@stylistic/eslint-plugin": "5.1.0",
"@types/express": "5.0.3",
"@types/node": "22.15.33",
"@types/node": "22.16.0",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.29.0",
"eslint": "9.30.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.4",
@@ -49,7 +49,7 @@
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"tslib": "2.8.1",
"typedoc": "0.28.5",
"typedoc": "0.28.7",
"typedoc-plugin-missing-exports": "4.0.0"
},
"optionalDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.95.0",
"version": "0.96.0",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -10,14 +10,14 @@
"url": "https://github.com/TriliumNext/Notes"
},
"dependencies": {
"@eslint/js": "9.29.0",
"@eslint/js": "9.30.1",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17",
"@fullcalendar/interaction": "6.1.17",
"@fullcalendar/list": "6.1.17",
"@fullcalendar/multimonth": "6.1.17",
"@fullcalendar/timegrid": "6.1.17",
"@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.18",
"@fullcalendar/multimonth": "6.1.18",
"@fullcalendar/timegrid": "6.1.18",
"@mermaid-js/layout-elk": "0.1.8",
"@mind-elixir/node-menu": "1.0.5",
"@popperjs/core": "2.11.8",
@@ -33,9 +33,9 @@
"dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0",
"draggabilly": "3.0.0",
"force-graph": "1.49.6",
"globals": "16.2.0",
"i18next": "25.2.1",
"force-graph": "1.50.1",
"globals": "16.3.0",
"i18next": "25.3.0",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
@@ -46,14 +46,15 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "15.0.12",
"mermaid": "11.7.0",
"mind-elixir": "4.6.1",
"marked": "16.0.0",
"mermaid": "11.8.0",
"mind-elixir": "4.6.2",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"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",

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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,10 +147,13 @@ class ContextMenu {
} else {
const $icon = $("<span>");
if ("uiIcon" in item && item.uiIcon) {
$icon.addClass(item.uiIcon);
} else {
$icon.append("&nbsp;");
if ("uiIcon" in item || "checked" in item) {
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
if (icon) {
$icon.addClass(icon);
} else {
$icon.append("&nbsp;");
}
}
const $link = $("<span>")

View File

@@ -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 });

View File

@@ -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";

View File

@@ -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");

View File

@@ -1,38 +1,40 @@
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") {
this.viewMode = new ListOrGridView(this.viewType, args);
} else if (this.viewType === "calendar") {
this.viewMode = new CalendarView(args);
} else {
this.viewMode = null;
switch (this.viewType) {
case "list":
case "grid":
this.viewMode = new ListOrGridView(this.viewType, args);
break;
case "calendar":
this.viewMode = new CalendarView(args);
break;
case "table":
this.viewMode = new TableView(args);
break;
default:
this.viewMode = null;
}
}
#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 {

View File

@@ -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) => {

View File

@@ -8,18 +8,35 @@ const SEPARATOR = { title: "----" };
async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// Please keep the order synced with the listing found also in aps/client/src/widgets/note_types.ts.
// The default note type (always the first item)
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
// Text notes group
{ title: t("note_types.book"), command, type: "book", uiIcon: "bx bx-book" },
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
// Graphic notes
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
// Map notes
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
// Misc note types
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
// Code notes
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
// Templates
...await getBuiltInTemplates(command),
...await getUserTemplates(command)
];

View File

@@ -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 {

View File

@@ -382,6 +382,10 @@ div.tn-tool-dialog {
/* DELETE NOTE PREVIEW DIALOG */
.delete-notes-dialog .modal-dialog {
--bs-modal-width: fit-content;
}
.delete-notes-list .note-path {
padding-left: 8px;
}

View File

@@ -46,6 +46,12 @@ div.promoted-attributes-container {
.image-properties > div:first-child > span > strong {
opacity: 0.65;
font-weight: 500;
vertical-align: top;
}
.note-info-widget-table td,
.file-properties-widget .file-table td {
vertical-align: top;
}
.file-properties-widget {

View File

@@ -71,7 +71,7 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
/* #endregion */
/* Matches when the left pane is collapsed */
:has(.layout-vertical #left-pane.hidden-int) {
#horizontal-main-container:has(#left-pane.hidden-int) {
--center-pane-border-radius: 0;
--tab-first-item-horiz-offset: 5px;
}

View File

@@ -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"
}
}

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -324,7 +324,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
}
const mapRootNoteId = this.getMapRootNoteId();
const data = await this.loadNotesAndRelations(mapRootNoteId);
const labelValues = (name: string) => this.note?.getLabels(name).map(l => l.value) ?? [];
const excludeRelations = labelValues("mapExcludeRelation");
const includeRelations = labelValues("mapIncludeRelation");
const data = await this.loadNotesAndRelations(mapRootNoteId, excludeRelations, includeRelations);
const nodeLinkRatio = data.nodes.length / data.links.length;
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
@@ -473,8 +479,10 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
ctx.restore();
}
async loadNotesAndRelations(mapRootNoteId: string): Promise<NotesAndRelationsData> {
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`);
async loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[]): Promise<NotesAndRelationsData> {
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`, {
excludeRelations, includeRelations
});
this.calculateNodeSizes(resp);

View File

@@ -19,6 +19,7 @@ interface NoteTypeMapping {
const NOTE_TYPES: NoteTypeMapping[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// Please keep the order synced with the listing found also in apps/client/src/services/note_types.ts.
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), selectable: true },

View File

@@ -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 }));
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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">) {

View File

@@ -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);
}

View File

@@ -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");

View 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;
}
}

View 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);
}
});
}

View 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();
}

View File

@@ -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 };
}

View File

@@ -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;
};

View 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;
}
}
}

View File

@@ -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;
};

View 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;
}

View File

@@ -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;
}
}

View 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 ?? "{}");
}
}

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [ "ESNext" ],
"outDir": "dist",
"types": [
"node"

View File

@@ -18,7 +18,7 @@
}
},
"devDependencies": {
"dotenv": "16.6.0",
"electron": "36.6.0"
"dotenv": "17.0.1",
"electron": "37.2.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.95.0",
"version": "0.96.0",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "main.cjs",
@@ -17,7 +17,7 @@
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.0",
"electron": "36.6.0",
"electron": "37.2.0",
"@electron-forge/cli": "7.8.1",
"@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-dmg": "7.8.1",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.0",
"electron": "36.6.0",
"electron": "37.2.0",
"fs-extra": "11.3.0"
},
"nx": {

View File

@@ -17,6 +17,6 @@
}
},
"devDependencies": {
"dotenv": "16.6.0"
"dotenv": "17.0.1"
}
}

View File

@@ -1,5 +1,5 @@
{
"dependencies": {
"better-sqlite3": "12.1.1"
"better-sqlite3": "12.2.0"
}
}

View File

@@ -1,10 +1,10 @@
{
"name": "@triliumnext/server",
"version": "0.95.0",
"version": "0.96.0",
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"dependencies": {
"better-sqlite3": "12.1.1"
"better-sqlite3": "12.2.0"
},
"devDependencies": {
"@electron/remote": "2.1.2",
@@ -24,7 +24,7 @@
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/mime-types": "3.0.1",
"@types/multer": "1.4.13",
"@types/multer": "2.0.0",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.16.0",
"@types/sax": "1.2.7",
@@ -39,7 +39,7 @@
"@types/ws": "8.18.1",
"@types/xml2js": "0.4.14",
"express-http-proxy": "2.1.1",
"@anthropic-ai/sdk": "0.55.0",
"@anthropic-ai/sdk": "0.55.1",
"@braintree/sanitize-url": "7.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
@@ -59,7 +59,7 @@
"debounce": "2.2.0",
"debug": "4.4.1",
"ejs": "3.1.10",
"electron": "36.6.0",
"electron": "37.2.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -74,7 +74,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.2.1",
"i18next": "25.3.0",
"i18next-fs-backend": "2.6.0",
"image-type": "6.0.0",
"ini": "5.0.0",
@@ -83,12 +83,12 @@
"jimp": "1.6.0",
"js-yaml": "4.1.0",
"jsdom": "26.1.0",
"marked": "15.0.12",
"marked": "16.0.0",
"mime-types": "3.0.1",
"multer": "2.0.1",
"normalize-strings": "1.1.1",
"ollama": "0.5.16",
"openai": "5.8.1",
"openai": "5.8.2",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
@@ -105,7 +105,7 @@
"tmp": "0.2.3",
"turndown": "7.2.0",
"unescape": "1.0.1",
"ws": "8.18.2",
"ws": "8.18.3",
"xml2js": "0.6.2",
"yauzl": "3.2.0"
},

File diff suppressed because one or more lines are too long

View File

@@ -42,5 +42,5 @@
This will export the notes in an unencrypted form, so if you reimport into
Trilium, make sure to re-protect these notes.</p>
<h2>Supported syntax</h2>
<p>See the dedicated page:&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/mHbBMPDPkVV5/Oau6X9rCuegd/_help_rJ9grSgoExl9">Supported syntax</a>
<p>See the dedicated page:&nbsp;<a class="reference-link" href="#root/_help_rJ9grSgoExl9">Supported syntax</a>
</p>

View File

@@ -41,7 +41,7 @@
Trilium-compatible syntax, but it will not export Trilium Notes into Markdown
files with this syntax.</p>
<aside class="admonition important">
<p>The path to pages in wikilinks is resolved relatively to the <em>import root </em>and
<p>The path to pages in wikilinks is resolved relatively to the <em>import root</em> and
not the current directory of the note. This is to be inline with other
platforms that use wikilinks such as SilverBullet.</p>
<p>The root path of the import is determined as follows:</p>

View 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&nbsp;<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&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;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>&nbsp;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&nbsp;<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&nbsp;
<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&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;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&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>&nbsp;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&nbsp;
<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&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>&nbsp;note.</p>
<p>Editing is also supported.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -6,4 +6,7 @@
of the same name:&nbsp;<a href="#root/_help_BCkXAVs63Ttv">Note Map (Link map, Tree map)</a>.</p>
<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>
(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>

View File

@@ -62,4 +62,4 @@ class="image image-style-align-center">
are currently no plans for adjusting it or allowing the user to customize
them.</p>
<h3>Markdown support</h3>
<p>See&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/mHbBMPDPkVV5/Oau6X9rCuegd/_help_rJ9grSgoExl9">Supported syntax</a>.</p>
<p>See&nbsp;<a class="reference-link" href="#root/_help_rJ9grSgoExl9">Supported syntax</a>.</p>

View File

@@ -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
});
}

View File

@@ -110,6 +110,11 @@ function getLinkMap(req: Request) {
const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy("excludeFromNoteMap");
let unfilteredNotes;
const toSet = (data: unknown) => new Set<string>(data instanceof Array ? data : []);
const excludeRelations = toSet(req.body.excludeRelations);
const includeRelations = toSet(req.body.includeRelations);
if (mapRootNote.type === "search") {
// for search notes, we want to consider the direct search results only without the descendants
unfilteredNotes = mapRootNote.getSearchResultNotes();
@@ -152,6 +157,10 @@ function getLinkMap(req: Request) {
}
return !parentNote.getChildNotes().find((childNote) => childNote.noteId === rel.value);
} else if (includeRelations.size != 0 && !includeRelations.has(rel.name)) {
return false;
} else if (excludeRelations.has(rel.name)) {
return false;
} else {
return true;
}

View File

@@ -255,8 +255,12 @@ export interface Api {
/**
* Returns week note for given date. If such a note doesn't exist, it is created.
*
* <p>
* If the calendar does not support week notes, this method will return `null`.
*
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
* @return an existing or newly created week note, or `null` if the calendar does not support week notes.
*/
getWeekNote(date: string, rootNote: BNote): BNote | null;

View File

@@ -45,6 +45,8 @@ export default [
{ type: "label", name: "pageSize" },
{ type: "label", name: "viewType" },
{ type: "label", name: "mapRootNoteId" },
{ type: "label", name: "mapExcludeRelation" },
{ type: "label", name: "mapIncludeRelation" },
{ type: "label", name: "bookmarkFolder" },
{ type: "label", name: "sorted" },
{ type: "label", name: "sortDirection" },

View File

@@ -75,6 +75,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
function getDataFileName(type: string | null, mime: string, baseFileName: string, existingFileNames: Record<string, number>): string {
let fileName = baseFileName.trim();
if (!fileName) {
fileName = "note";
}
// Crop fileName to avoid its length exceeding 30 and prevent cutting into the extension.
if (fileName.length > 30) {
@@ -366,7 +369,7 @@ ${markdownContent}`;
function saveNote(noteMeta: NoteMeta, filePathPrefix: string) {
log.info(`Exporting note '${noteMeta.noteId}'`);
if (!noteMeta.noteId || !noteMeta.title) {
if (!noteMeta.noteId || noteMeta.title === undefined) {
throw new Error("Missing note meta.");
}
@@ -515,97 +518,108 @@ ${markdownContent}`;
archive.append(cssContent, { name: cssMeta.dataFileName });
}
const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
if (!rootMeta) {
throw new Error("Unable to create root meta.");
}
try {
const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
if (!rootMeta) {
throw new Error("Unable to create root meta.");
}
const metaFile: NoteMetaFile = {
formatVersion: 2,
appVersion: packageInfo.version,
files: [rootMeta]
};
let navigationMeta: NoteMeta | null = null;
let indexMeta: NoteMeta | null = null;
let cssMeta: NoteMeta | null = null;
if (format === "html") {
navigationMeta = {
noImport: true,
dataFileName: "navigation.html"
const metaFile: NoteMetaFile = {
formatVersion: 2,
appVersion: packageInfo.version,
files: [rootMeta]
};
metaFile.files.push(navigationMeta);
let navigationMeta: NoteMeta | null = null;
let indexMeta: NoteMeta | null = null;
let cssMeta: NoteMeta | null = null;
indexMeta = {
noImport: true,
dataFileName: "index.html"
};
if (format === "html") {
navigationMeta = {
noImport: true,
dataFileName: "navigation.html"
};
metaFile.files.push(indexMeta);
metaFile.files.push(navigationMeta);
cssMeta = {
noImport: true,
dataFileName: "style.css"
};
indexMeta = {
noImport: true,
dataFileName: "index.html"
};
metaFile.files.push(cssMeta);
}
metaFile.files.push(indexMeta);
for (const noteMeta of Object.values(noteIdToMeta)) {
// filter out relations which are not inside this export
noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
if (attr.type !== "relation") {
return true;
} else if (attr.value in noteIdToMeta) {
return true;
} else if (attr.value === "root" || attr.value?.startsWith("_")) {
// relations to "named" noteIds can be preserved
return true;
} else {
return false;
cssMeta = {
noImport: true,
dataFileName: "style.css"
};
metaFile.files.push(cssMeta);
}
for (const noteMeta of Object.values(noteIdToMeta)) {
// filter out relations which are not inside this export
noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
if (attr.type !== "relation") {
return true;
} else if (attr.value in noteIdToMeta) {
return true;
} else if (attr.value === "root" || attr.value?.startsWith("_")) {
// relations to "named" noteIds can be preserved
return true;
} else {
return false;
}
});
}
if (!rootMeta) {
// corner case of disabled export for exported note
if ("sendStatus" in res) {
res.sendStatus(400);
}
});
}
return;
}
const metaFileJson = JSON.stringify(metaFile, null, "\t");
archive.append(metaFileJson, { name: "!!!meta.json" });
saveNote(rootMeta, "");
if (format === "html") {
if (!navigationMeta || !indexMeta || !cssMeta) {
throw new Error("Missing meta.");
}
saveNavigation(rootMeta, navigationMeta);
saveIndex(rootMeta, indexMeta);
saveCss(rootMeta, cssMeta);
}
const note = branch.getNote();
const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected() || "note"}.zip`;
if (setHeaders && "setHeader" in res) {
res.setHeader("Content-Disposition", getContentDisposition(zipFileName));
res.setHeader("Content-Type", "application/zip");
}
archive.pipe(res);
await archive.finalize();
taskContext.taskSucceeded();
} catch (e: unknown) {
const message = `Export failed with error: ${e instanceof Error ? e.message : String(e)}`;
log.error(message);
taskContext.reportError(message);
if (!rootMeta) {
// corner case of disabled export for exported note
if ("sendStatus" in res) {
res.sendStatus(400);
res.removeHeader("Content-Disposition");
res.removeHeader("Content-Type");
res.status(500).send(message);
}
return;
}
const metaFileJson = JSON.stringify(metaFile, null, "\t");
archive.append(metaFileJson, { name: "!!!meta.json" });
saveNote(rootMeta, "");
if (format === "html") {
if (!navigationMeta || !indexMeta || !cssMeta) {
throw new Error("Missing meta.");
}
saveNavigation(rootMeta, navigationMeta);
saveIndex(rootMeta, indexMeta);
saveCss(rootMeta, cssMeta);
}
const note = branch.getNote();
const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
if (setHeaders && "setHeader" in res) {
res.setHeader("Content-Disposition", getContentDisposition(zipFileName));
res.setHeader("Content-Type", "application/zip");
}
archive.pipe(res);
await archive.finalize();
taskContext.taskSucceeded();
}
async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string, zipExportOptions?: AdvancedExportOptions) {

View File

@@ -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"
}
]
}
]
};

View File

@@ -29,16 +29,18 @@
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6"
"vite": "^7.0.0"
},
"dependencies": {
"@inlang/paraglide-js": "^2.0.0"
},
"nx": {
"typecheck": {
"dependsOn": [
"build"
]
"targets": {
"typecheck": {
"dependsOn": [
"build"
]
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.95.0",
"appVersion": "0.96.0",
"files": [
{
"isClone": false,

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.95.0",
"appVersion": "0.96.0",
"files": [
{
"isClone": false,
@@ -61,6 +61,32 @@
"attachments": [],
"dirFileName": "Release Notes",
"children": [
{
"isClone": false,
"noteId": "mYXFde3LuNR7",
"notePath": [
"hD3V4hiu2VW4",
"mYXFde3LuNR7"
],
"title": "v0.96.0",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "template",
"value": "wyurrlcDl416",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "v0.96.0.md",
"attachments": []
},
{
"isClone": false,
"noteId": "jthwbL0FdaeU",
@@ -69,7 +95,7 @@
"jthwbL0FdaeU"
],
"title": "v0.95.0",
"notePosition": 10,
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -95,7 +121,7 @@
"7HGYsJbLuhnv"
],
"title": "v0.94.1",
"notePosition": 20,
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -121,7 +147,7 @@
"Neq53ujRGBqv"
],
"title": "v0.94.0",
"notePosition": 30,
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -147,7 +173,7 @@
"VN3xnce1vLkX"
],
"title": "v0.93.0",
"notePosition": 40,
"notePosition": 50,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -165,7 +191,7 @@
"WRaBfQqPr6qo"
],
"title": "v0.92.7",
"notePosition": 50,
"notePosition": 60,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -191,7 +217,7 @@
"a2rwfKNmUFU1"
],
"title": "v0.92.6",
"notePosition": 60,
"notePosition": 70,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -209,7 +235,7 @@
"fEJ8qErr0BKL"
],
"title": "v0.92.5-beta",
"notePosition": 70,
"notePosition": 80,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -227,7 +253,7 @@
"kkkZQQGSXjwy"
],
"title": "v0.92.4",
"notePosition": 80,
"notePosition": 90,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -245,7 +271,7 @@
"vAroNixiezaH"
],
"title": "v0.92.3-beta",
"notePosition": 90,
"notePosition": 100,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -263,7 +289,7 @@
"mHEq1wxAKNZd"
],
"title": "v0.92.2-beta",
"notePosition": 100,
"notePosition": 110,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -281,7 +307,7 @@
"IykjoAmBpc61"
],
"title": "v0.92.1-beta",
"notePosition": 110,
"notePosition": 120,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -299,7 +325,7 @@
"dq2AJ9vSBX4Y"
],
"title": "v0.92.0-beta",
"notePosition": 120,
"notePosition": 130,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -317,7 +343,7 @@
"3a8aMe4jz4yM"
],
"title": "v0.91.6",
"notePosition": 130,
"notePosition": 140,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -335,7 +361,7 @@
"8djQjkiDGESe"
],
"title": "v0.91.5",
"notePosition": 140,
"notePosition": 150,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -353,7 +379,7 @@
"OylxVoVJqNmr"
],
"title": "v0.91.4-beta",
"notePosition": 150,
"notePosition": 160,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -371,7 +397,7 @@
"tANGQDvnyhrj"
],
"title": "v0.91.3-beta",
"notePosition": 160,
"notePosition": 170,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -389,7 +415,7 @@
"hMoBfwSoj1SC"
],
"title": "v0.91.2-beta",
"notePosition": 170,
"notePosition": 180,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -407,7 +433,7 @@
"a2XMSKROCl9z"
],
"title": "v0.91.1-beta",
"notePosition": 180,
"notePosition": 190,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -425,7 +451,7 @@
"yqXFvWbLkuMD"
],
"title": "v0.90.12",
"notePosition": 190,
"notePosition": 200,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -443,7 +469,7 @@
"veS7pg311yJP"
],
"title": "v0.90.11-beta",
"notePosition": 200,
"notePosition": 210,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -461,7 +487,7 @@
"sq5W9TQxRqMq"
],
"title": "v0.90.10-beta",
"notePosition": 210,
"notePosition": 220,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -479,7 +505,7 @@
"yFEGVCUM9tPx"
],
"title": "v0.90.9-beta",
"notePosition": 220,
"notePosition": 230,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -497,7 +523,7 @@
"o4wAGqOQuJtV"
],
"title": "v0.90.8",
"notePosition": 230,
"notePosition": 240,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -530,7 +556,7 @@
"i4A5g9iOg9I0"
],
"title": "v0.90.7-beta",
"notePosition": 240,
"notePosition": 250,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -548,7 +574,7 @@
"ThNf2GaKgXUs"
],
"title": "v0.90.6-beta",
"notePosition": 250,
"notePosition": 260,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -566,7 +592,7 @@
"G4PAi554kQUr"
],
"title": "v0.90.5-beta",
"notePosition": 260,
"notePosition": 270,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -593,7 +619,7 @@
"zATRobGRCmBn"
],
"title": "v0.90.4",
"notePosition": 270,
"notePosition": 280,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -611,7 +637,7 @@
"sCDLf8IKn3Iz"
],
"title": "v0.90.3",
"notePosition": 280,
"notePosition": 290,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -629,7 +655,7 @@
"VqqyBu4AuTjC"
],
"title": "v0.90.2-beta",
"notePosition": 290,
"notePosition": 300,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -647,7 +673,7 @@
"RX3Nl7wInLsA"
],
"title": "v0.90.1-beta",
"notePosition": 300,
"notePosition": 310,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -665,7 +691,7 @@
"GyueACukPWjk"
],
"title": "v0.90.0-beta",
"notePosition": 310,
"notePosition": 320,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -683,7 +709,7 @@
"wyurrlcDl416"
],
"title": "Release Template",
"notePosition": 320,
"notePosition": 330,
"prefix": null,
"isExpanded": false,
"type": "text",

View File

@@ -1,6 +1,6 @@
# v0.94.1
> [!NOTE]
> _Trilium Notes_ will rebrand itself back to Trilium Notes since @zadam was kind enough to give us the original name. See [#2190](https://github.com/orgs/TriliumNext/discussions/2190) for more info. This will probably be the "last" version branded as Trilium Notes_.
> _Trilium Notes_ will rebrand itself back to Trilium Notes since @zadam was kind enough to give us the original name. See [#2190](https://github.com/orgs/TriliumNext/discussions/2190) for more info. This will probably be the "last" version branded as Trilium Notes\_.
> [!IMPORTANT]
> If you enjoyed this release, consider showing a token of appreciation by:

View File

@@ -0,0 +1,50 @@
# 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/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
* Thanks to a partnership with CKEditor, we now have a set of features that would otherwise be available on a commercial license only.
* Slash commands for easy commands via the keyboard.
* Text snippets to insert reusable pieces of text (similar to templates, but for blocks of text content).
* For more information, see the user guide → Note Types → Text → Premium features.
## 🐞 Bugfixes
* [“Insert note after” long-press dialog doesnt create Note](https://github.com/TriliumNext/Notes/issues/2246)
* Code notes: user's font selection not respected.
* [Windows V0.95.0 Client Failed to sync with server (use of double-slashes)](https://github.com/TriliumNext/Notes/issues/2339) by @perfectra1n
* Desktop client not working on older Linux distros
* [NOT NULL constraint failed: revisions.title when saving an empty note](https://github.com/TriliumNext/Trilium/issues/6103)
## ✨ Improvements
* [Elixir language syntax highlighting for text notes](https://github.com/TriliumNext/Notes/pull/2327) (by @jshprentz) and code notes.
* [Autocomplete: support specifying path when creating a new note](https://github.com/TriliumNext/Notes/pull/2342) by @SiriusXT
* Markdown import: basic support for importing wikilinks
* Text notes:
* Allow disabling emoji auto-completion from settings.
* Allow disabling note auto-completion from settings.
* [Backend scripts: re-enable dayjs plugins by default](https://github.com/TriliumNext/Trilium/issues/6080)
## 📖 Documentation
* [regex search / Nix flake / restore dev docs](https://github.com/TriliumNext/Notes/pull/2341) by @FliegendeWurst
* New premium features in text note type.
## 🌍 Internationalization
* Spanish improvements by @hasecilu
## 🛠️ Technical updates
* flake: fix Electron version, fix Wayland support, fix source filter by @FliegendeWurst
* Improvements to the landing page (under development) by @FliegendeWurst
* Updated Node.js to v22.17.0
* Updated CKEditor to v45.2.1

View File

@@ -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"
}
]
}
]
}
@@ -4350,6 +4430,13 @@
"type": "text",
"mime": "text/markdown",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "rJ9grSgoExl9",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "shareAlias",
@@ -4363,13 +4450,6 @@
"value": "bx bxl-markdown",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "rJ9grSgoExl9",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
@@ -5113,23 +5193,23 @@
{
"type": "relation",
"name": "internalLink",
"value": "nRhnJkTT8cPs",
"value": "rJ9grSgoExl9",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "nRhnJkTT8cPs",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-info-circle",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "rJ9grSgoExl9",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -3,4 +3,6 @@
A Note map is a note type which displays a standalone version of the feature of the same name: [Note Map (Link map, Tree map)](../Advanced%20Usage/Note%20Map%20\(Link%20map%2C%20Tree%20map\).md).
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).
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).
The labels `mapIncludeRelation` and `mapExcludeRelation`, if set, filter the note map to include only the specified relations or to exclude the specified relations, respectively.

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/source",
"version": "0.95.0",
"version": "0.96.0",
"description": "Build your personal knowledge base with Trilium Notes",
"directories": {
"doc": "docs"
@@ -27,20 +27,20 @@
"private": true,
"devDependencies": {
"@electron/rebuild": "4.0.1",
"@nx/devkit": "21.2.1",
"@nx/esbuild": "21.2.1",
"@nx/eslint": "21.2.1",
"@nx/eslint-plugin": "21.2.1",
"@nx/express": "21.2.1",
"@nx/js": "21.2.1",
"@nx/node": "21.2.1",
"@nx/playwright": "21.2.1",
"@nx/vite": "21.2.1",
"@nx/web": "21.2.1",
"@nx/devkit": "21.2.2",
"@nx/esbuild": "21.2.2",
"@nx/eslint": "21.2.2",
"@nx/eslint-plugin": "21.2.2",
"@nx/express": "21.2.2",
"@nx/js": "21.2.2",
"@nx/node": "21.2.2",
"@nx/playwright": "21.2.2",
"@nx/vite": "21.2.2",
"@nx/web": "21.2.2",
"@playwright/test": "^1.36.0",
"@triliumnext/server": "workspace:*",
"@types/express": "^5.0.0",
"@types/node": "22.15.33",
"@types/node": "22.16.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0",
"chalk": "5.4.1",
@@ -54,15 +54,15 @@
"jiti": "2.4.2",
"jsdom": "~26.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "21.2.1",
"nx": "21.2.2",
"react-refresh": "^0.17.0",
"rollup-plugin-webpack-stats": "2.0.7",
"rollup-plugin-webpack-stats": "2.1.0",
"tslib": "^2.3.0",
"tsx": "4.20.3",
"typescript": "~5.8.0",
"typescript-eslint": "^8.19.0",
"upath": "2.0.1",
"vite": "^6.0.0",
"vite": "^7.0.0",
"vite-plugin-dts": "~4.5.0",
"vitest": "^3.0.0"
},
@@ -89,7 +89,7 @@
"@nx/js": "patches/@nx__js.patch"
},
"overrides": {
"mermaid": "11.7.0",
"mermaid": "11.8.0",
"preact": "10.26.9",
"roughjs": "4.6.6",
"@types/express-serve-static-core": "5.0.6",

View File

@@ -30,7 +30,7 @@
"@codemirror/lang-xml": "6.1.0",
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.11",
"@codemirror/view": "6.37.2",
"@codemirror/view": "6.38.0",
"@fsegurai/codemirror-theme-abcdef": "6.2.0",
"@fsegurai/codemirror-theme-abyss": "6.2.0",
"@fsegurai/codemirror-theme-android-studio": "6.2.0",
@@ -62,6 +62,6 @@
"codemirror-lang-elixir": "4.0.0",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "9.29.0"
"eslint-linter-browserify": "9.30.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/commons",
"version": "0.95.0",
"version": "0.96.0",
"description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.",
"private": true,
"type": "module",

View File

@@ -26,7 +26,7 @@
"@types/swagger-ui": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"dotenv": "^16.3.1",
"dotenv": "^17.0.0",
"esbuild": "^0.25.0",
"eslint": "^9.0.0",
"highlight.js": "^11.8.0",

3776
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff