Compare commits
208 Commits
feat/extra
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e22a5636 | ||
|
|
256ad05d2d | ||
|
|
1b0a53a441 | ||
|
|
430ef62a2d | ||
|
|
50aeda8ee8 | ||
|
|
1520c696a3 | ||
|
|
f63f6244a1 | ||
|
|
8611d4a67a | ||
|
|
c48bd9a5c3 | ||
|
|
dba985b308 | ||
|
|
a51a831fe8 | ||
|
|
44142e980d | ||
|
|
7f83226f84 | ||
|
|
e3fdae8932 | ||
|
|
78c62be823 | ||
|
|
e51cea88bf | ||
|
|
d7409bec49 | ||
|
|
17b1f599ff | ||
|
|
81c85d712e | ||
|
|
2eae8bbb64 | ||
|
|
2a61f51e06 | ||
|
|
3e3c3e3bb4 | ||
|
|
7b41a89b8e | ||
|
|
36429da6da | ||
|
|
30f6ab5976 | ||
|
|
99a46f2a85 | ||
|
|
6754b1f2e1 | ||
|
|
122ad2b771 | ||
|
|
714e8ade1a | ||
|
|
4a6ea38be0 | ||
|
|
8bc7f0b71f | ||
|
|
9a912c16ad | ||
|
|
10a84a1356 | ||
|
|
901201a7af | ||
|
|
a57a1dfc47 | ||
|
|
577780cb90 | ||
|
|
b45eef9140 | ||
|
|
907853bbba | ||
|
|
17f3ffd00c | ||
|
|
8b86e17ac8 | ||
|
|
d6b6832a1d | ||
|
|
9dfc1cdc4c | ||
|
|
673c39d798 | ||
|
|
8ca84d183c | ||
|
|
9577aa2abe | ||
|
|
227be184ac | ||
|
|
d677f65eeb | ||
|
|
92f86bcca2 | ||
|
|
2015068d9e | ||
|
|
8a280c2f9d | ||
|
|
34fd6f9502 | ||
|
|
02acb36e47 | ||
|
|
280c0e0348 | ||
|
|
8528f0d848 | ||
|
|
917e881faa | ||
|
|
96b1efcfdc | ||
|
|
794e03b2cb | ||
|
|
a285c46b97 | ||
|
|
4da6294ef2 | ||
|
|
16ed9a7e8e | ||
|
|
798efbc22f | ||
|
|
60c789b6c7 | ||
|
|
f83d95136d | ||
|
|
f96ed0af26 | ||
|
|
1539664026 | ||
|
|
8aff775d0e | ||
|
|
94248eafe9 | ||
|
|
02335bba3f | ||
|
|
dad9578b83 | ||
|
|
c043788b09 | ||
|
|
e5bc416b46 | ||
|
|
c97f52da36 | ||
|
|
1661c3292a | ||
|
|
4e80c07630 | ||
|
|
d43309947e | ||
|
|
c7cc702c4a | ||
|
|
c304753ffc | ||
|
|
da59c14231 | ||
|
|
e0b3e41c9e | ||
|
|
5d1a63bce0 | ||
|
|
84cf4ef4a3 | ||
|
|
ec4b6f0a90 | ||
|
|
60dbdbeb71 | ||
|
|
418a546583 | ||
|
|
e6380b87b6 | ||
|
|
1d3d214101 | ||
|
|
a38067560b | ||
|
|
eac7235199 | ||
|
|
4c55e857b8 | ||
|
|
e33950e000 | ||
|
|
fc0ccbfcf5 | ||
|
|
4a82bbb035 | ||
|
|
57d894e765 | ||
|
|
97dfad419c | ||
|
|
bfc521fdc0 | ||
|
|
197fa90176 | ||
|
|
0844914e11 | ||
|
|
c376b0bbe2 | ||
|
|
4491086c55 | ||
|
|
791697369d | ||
|
|
28d0bfd229 | ||
|
|
8182a04eae | ||
|
|
711828d6b4 | ||
|
|
69e88c1d9f | ||
|
|
748b87da9a | ||
|
|
94dca4cd87 | ||
|
|
7179701e0f | ||
|
|
af5061646c | ||
|
|
9c4163ad3a | ||
|
|
46c3f5296a | ||
|
|
ebadcfd844 | ||
|
|
b7d4947462 | ||
|
|
a599526dea | ||
|
|
c4f166fe12 | ||
|
|
a8ae91aa3b | ||
|
|
82b3692acb | ||
|
|
432c054b68 | ||
|
|
dcd8bfa255 | ||
|
|
c287a2ae97 | ||
|
|
8fdadb3798 | ||
|
|
3fce4fc66c | ||
|
|
d83d7ed106 | ||
|
|
1b812f1886 | ||
|
|
56fcc7adcc | ||
|
|
fb0c7359f1 | ||
|
|
4c4e5b85e9 | ||
|
|
476247beb5 | ||
|
|
2c87f609f3 | ||
|
|
bc79ff6845 | ||
|
|
f10373d54f | ||
|
|
630d16b722 | ||
|
|
769f3db21c | ||
|
|
c6896a4b33 | ||
|
|
7c18025098 | ||
|
|
6ae74b3181 | ||
|
|
2ecfbbf284 | ||
|
|
781de9a1fb | ||
|
|
6972a4b901 | ||
|
|
52ed1750ac | ||
|
|
9010e0b1ce | ||
|
|
5053e74447 | ||
|
|
f294276849 | ||
|
|
0740788cc8 | ||
|
|
9bac07ce62 | ||
|
|
3d8289d394 | ||
|
|
5a60fdad8a | ||
|
|
62cca5a96b | ||
|
|
74548d638e | ||
|
|
e0ccf30f4f | ||
|
|
d2c6081537 | ||
|
|
bd933f2c4c | ||
|
|
1d898d618e | ||
|
|
11c8e5b3b2 | ||
|
|
fe4c3ffecb | ||
|
|
334a96186e | ||
|
|
34b2df705b | ||
|
|
5a7fc1c8b6 | ||
|
|
fabab6abb1 | ||
|
|
0c9c20c0c5 | ||
|
|
67cc1113b1 | ||
|
|
3aacd255f4 | ||
|
|
ccfda21413 | ||
|
|
46c88506cc | ||
|
|
51157e1979 | ||
|
|
bfb6d975ff | ||
|
|
aa01bc1457 | ||
|
|
5600f1b7b1 | ||
|
|
a169db807c | ||
|
|
f40348daff | ||
|
|
f63042ef87 | ||
|
|
d148c9d1c6 | ||
|
|
f72929ca13 | ||
|
|
cc3e3ca4d4 | ||
|
|
8fad664a6d | ||
|
|
f1946c1386 | ||
|
|
ea8bd0136f | ||
|
|
4a3f72ae50 | ||
|
|
c944762ef6 | ||
|
|
f6924d7fda | ||
|
|
3a0880fcd6 | ||
|
|
df62dc87b2 | ||
|
|
d42679315e | ||
|
|
2a19be5ab6 | ||
|
|
33bbe994d7 | ||
|
|
04c598caea | ||
|
|
3577688bf9 | ||
|
|
e0439655df | ||
|
|
84f944f78a | ||
|
|
022b6df959 | ||
|
|
9e3e92669f | ||
|
|
35b96a71fc | ||
|
|
4b78de6726 | ||
|
|
4771e02909 | ||
|
|
03dffdb65f | ||
|
|
859a3948cd | ||
|
|
161aa625e6 | ||
|
|
28fd945e80 | ||
|
|
748fb0bf05 | ||
|
|
98e1d0afd9 | ||
|
|
9c61ce1835 | ||
|
|
c3a5705be0 | ||
|
|
9d0fa9f7ca | ||
|
|
cc4ceb975e | ||
|
|
8a6495a0bd | ||
|
|
1d95392d22 | ||
|
|
93dd08d629 | ||
|
|
a1c0314334 | ||
|
|
3ecdcd9ea0 |
@@ -9,9 +9,9 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.5",
|
||||
"@redocly/cli": "2.14.7",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.5.1",
|
||||
"@preact/signals": "2.6.1",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@zumer/snapdom": "2.0.1",
|
||||
"@zumer/snapdom": "2.0.2",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
@@ -44,9 +44,9 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.27",
|
||||
@@ -56,7 +56,7 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.5.0",
|
||||
"mind-elixir": "5.6.1",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.2",
|
||||
@@ -78,9 +78,9 @@
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.1.0",
|
||||
"lightningcss": "1.30.2",
|
||||
"happy-dom": "20.3.7",
|
||||
"lightningcss": "1.31.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
"vite-plugin-static-copy": "3.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import type { NativeImage, TouchBar } from "electron";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
@@ -410,7 +410,7 @@ type EventMappings = {
|
||||
addNewLabel: CommandData;
|
||||
addNewRelation: CommandData;
|
||||
sqlQueryResults: CommandData & {
|
||||
results: SqlExecuteResults;
|
||||
response: SqlExecuteResponse;
|
||||
};
|
||||
readOnlyTemporarilyDisabled: {
|
||||
noteContext: NoteContext;
|
||||
@@ -542,7 +542,6 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
||||
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||
tabManager!: TabManager;
|
||||
@@ -551,11 +550,10 @@ export class AppContext extends Component {
|
||||
|
||||
lastSearchString?: string;
|
||||
|
||||
constructor(isMainWindow: boolean, windowId: string) {
|
||||
constructor(isMainWindow: boolean) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
this.windowId = windowId;
|
||||
// non-widget/layout components needed for the application
|
||||
this.components = [];
|
||||
this.beforeUnloadListeners = [];
|
||||
@@ -685,7 +683,8 @@ export class AppContext extends Component {
|
||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on("beforeunload", () => {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import utils from "../services/utils.js";
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
import bundleService from "../services/bundle.js";
|
||||
import dateNoteService from "../services/date_notes.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import linkService from "../services/link.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import utils from "../services/utils.js";
|
||||
import ws from "../services/ws.js";
|
||||
import appContext, { type NoteCommandData } from "./app_context.js";
|
||||
import Component from "./component.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import ws from "../services/ws.js";
|
||||
import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@@ -142,15 +143,14 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
|
||||
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
||||
const extraWindowId = utils.randomString(4);
|
||||
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
|
||||
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||
ipcRenderer.send("create-extra-window", { extraWindowHash });
|
||||
} else {
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||
|
||||
window.open(url, "", "width=1000,height=800");
|
||||
}
|
||||
@@ -188,13 +188,8 @@ export default class Entrypoints extends Component {
|
||||
} else if (note.mime.endsWith("env=backend")) {
|
||||
await server.post(`script/run/${note.noteId}`);
|
||||
} else if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
||||
}
|
||||
|
||||
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
|
||||
const response = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
|
||||
}
|
||||
|
||||
toastService.showMessage(t("entrypoints.note-executed"));
|
||||
|
||||
@@ -11,8 +11,6 @@ import linkService from "../services/link.js";
|
||||
import type { EventData } from "./app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
const MAX_SAVED_WINDOWS = 10;
|
||||
|
||||
interface TabState {
|
||||
contexts: NoteContext[];
|
||||
position: number;
|
||||
@@ -27,13 +25,6 @@ interface NoteContextState {
|
||||
viewScope: Record<string, any>;
|
||||
}
|
||||
|
||||
interface WindowState {
|
||||
windowId: string;
|
||||
createdAt: number;
|
||||
closedAt: number;
|
||||
contexts: NoteContextState[];
|
||||
}
|
||||
|
||||
export default class TabManager extends Component {
|
||||
public children: NoteContext[];
|
||||
public mutex: Mutex;
|
||||
@@ -50,6 +41,9 @@ export default class TabManager extends Component {
|
||||
this.recentlyClosedTabs = [];
|
||||
|
||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||
if (!appContext.isMainWindow) {
|
||||
return;
|
||||
}
|
||||
if (options.is("databaseReadonly")) {
|
||||
return;
|
||||
}
|
||||
@@ -58,21 +52,9 @@ export default class TabManager extends Component {
|
||||
.map((nc) => nc.getPojoState())
|
||||
.filter((t) => !!t);
|
||||
|
||||
// Update the current window’s openNoteContexts in options
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId);
|
||||
if (win) {
|
||||
win.contexts = openNoteContexts;
|
||||
} else {
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: openNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
await server.put("options", {
|
||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
||||
});
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -87,13 +69,8 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
async loadTabs() {
|
||||
// Get the current window’s openNoteContexts
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const currentWin = savedWindows.find(w => w.windowId === appContext.windowId);
|
||||
const openNoteContexts = currentWin ? currentWin.contexts : undefined;
|
||||
|
||||
try {
|
||||
const noteContextsToOpen = openNoteContexts || [];
|
||||
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
@@ -142,51 +119,6 @@ export default class TabManager extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
// Save window contents
|
||||
if (currentWin as WindowState) {
|
||||
currentWin.createdAt = Date.now();
|
||||
currentWin.closedAt = 0;
|
||||
currentWin.contexts = filteredNoteContexts;
|
||||
} else {
|
||||
if (savedWindows?.length >= MAX_SAVED_WINDOWS) {
|
||||
// Filter out the oldest entry
|
||||
// 1) Never remove the "main" window
|
||||
// 2) Prefer removing the oldest closed window (closedAt !== 0)
|
||||
// 3) If no closed window exists, remove the window with the oldest created window
|
||||
let oldestClosedIndex = -1;
|
||||
let oldestClosedTime = Infinity;
|
||||
let oldestCreatedIndex = -1;
|
||||
let oldestCreatedTime = Infinity;
|
||||
savedWindows.forEach((w: WindowState, i: number) => {
|
||||
if (w.windowId === "main") return;
|
||||
if (w.closedAt !== 0) {
|
||||
if (w.closedAt < oldestClosedTime) {
|
||||
oldestClosedTime = w.closedAt;
|
||||
oldestClosedIndex = i;
|
||||
}
|
||||
} else {
|
||||
if (w.createdAt < oldestCreatedTime) {
|
||||
oldestCreatedTime = w.createdAt;
|
||||
oldestCreatedIndex = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex;
|
||||
if (indexToRemove !== -1) {
|
||||
savedWindows.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: filteredNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
|
||||
// if there's a notePath in the URL, make sure it's open and active
|
||||
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
|
||||
if (parsedFromUrl.notePath) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MIME_TYPES_DICT } from "@triliumnext/commons";
|
||||
import { getNoteIcon } from "@triliumnext/commons";
|
||||
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
@@ -13,25 +13,6 @@ import type { AttributeType, default as FAttribute } from "./fattribute.js";
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
|
||||
export const NOTE_TYPE_ICONS = {
|
||||
file: "bx bx-file",
|
||||
image: "bx bx-image",
|
||||
code: "bx bx-code",
|
||||
render: "bx bx-extension",
|
||||
search: "bx bx-file-find",
|
||||
relationMap: "bx bxs-network-chart",
|
||||
book: "bx bx-book",
|
||||
noteMap: "bx bxs-network-chart",
|
||||
mermaid: "bx bx-selection",
|
||||
canvas: "bx bx-pen",
|
||||
webView: "bx bx-globe-alt",
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
aiChat: "bx bx-bot"
|
||||
};
|
||||
|
||||
/**
|
||||
* There are many different Note types, some of which are entirely opaque to the
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
@@ -582,32 +563,18 @@ export default class FNote {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
} else if (workspaceIconClass) {
|
||||
return workspaceIconClass;
|
||||
} else if (this.noteId === "root") {
|
||||
return "bx bx-home-alt-2";
|
||||
}
|
||||
if (this.noteId === "_share") {
|
||||
return "bx bx-share-alt";
|
||||
} else if (this.type === "text") {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
return "bx bx-note";
|
||||
} else if (this.type === "code") {
|
||||
const correspondingMimeType = MIME_TYPES_DICT.find(m => m.mime === this.mime);
|
||||
return correspondingMimeType?.icon ?? NOTE_TYPE_ICONS.code;
|
||||
}
|
||||
return NOTE_TYPE_ICONS[this.type];
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
workspaceIconClass,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
return `tn-icon ${icon}`;
|
||||
}
|
||||
|
||||
getColorClass() {
|
||||
|
||||
@@ -16,6 +16,17 @@ async function initJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
|
||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
||||
($ as any).isArray = Array.isArray;
|
||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
||||
($ as any).isPlainObject = function(obj: any) {
|
||||
if (obj == null || typeof obj !== 'object') { return false; }
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) { return true; }
|
||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
||||
return typeof Ctor === 'function' && Ctor === Object;
|
||||
};
|
||||
}
|
||||
|
||||
async function setupGlob() {
|
||||
@@ -39,22 +50,25 @@ async function loadBootstrapCss() {
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
|
||||
const cssToLoad: string[] = [];
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
if (device !== "print") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
@@ -71,7 +85,7 @@ function loadIcons() {
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob;
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
`heading-style-${headingStyle}`,
|
||||
@@ -79,8 +93,7 @@ function setBodyAttributes() {
|
||||
`platform-${platform}`,
|
||||
isElectron && "electron",
|
||||
hasNativeTitleBar && "native-titlebar",
|
||||
hasBackgroundEffects && "background-effects",
|
||||
!isMainWindow && 'extra-window'
|
||||
hasBackgroundEffects && "background-effects"
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const classToSet of classesToSet) {
|
||||
@@ -92,10 +105,17 @@ function setBodyAttributes() {
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
switch (glob.device) {
|
||||
case "mobile":
|
||||
await import("./mobile.js");
|
||||
break;
|
||||
case "print":
|
||||
await import("./print.js");
|
||||
break;
|
||||
case "desktop":
|
||||
default:
|
||||
await import("./desktop.js");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||
@@ -163,11 +161,9 @@ export default class DesktopLayout {
|
||||
.child(<SharedInfo />)
|
||||
)
|
||||
.optChild(!isNewLayout, <PromotedAttributes />)
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
)
|
||||
.child(<ApiLog />)
|
||||
|
||||
@@ -29,7 +29,9 @@ async function main() {
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
render(<App note={note} noteId={noteId} />, document.body);
|
||||
const bodyWrapper = document.createElement("div");
|
||||
render(<App note={note} noteId={noteId} />, bodyWrapper);
|
||||
document.body.appendChild(bodyWrapper);
|
||||
}
|
||||
|
||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||
|
||||
@@ -8,6 +8,17 @@ async function loadBootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
||||
($ as any).isArray = Array.isArray;
|
||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
||||
($ as any).isPlainObject = function(obj: any) {
|
||||
if (obj == null || typeof obj !== 'object') { return false; }
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) { return true; }
|
||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
||||
return typeof Ctor === 'function' && Ctor === Object;
|
||||
};
|
||||
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
await loadBootstrap();
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("removes boolean normally without inheritance", async () => {
|
||||
@@ -91,7 +91,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "false",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("overrides boolean with inherited false", async () => {
|
||||
@@ -112,7 +112,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("deletes override boolean with inherited false with already existing value", async () => {
|
||||
@@ -134,6 +134,6 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,13 +14,13 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
|
||||
});
|
||||
}
|
||||
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false, componentId?: string) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
isInheritable,
|
||||
}, componentId);
|
||||
}
|
||||
|
||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
@@ -117,15 +117,15 @@ function removeOwnedRelationByName(note: FNote, relationName: string) {
|
||||
* @param name the name of the attribute to set.
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
export 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, componentId?: string) {
|
||||
if (value !== null && value !== undefined) {
|
||||
// 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 }, componentId);
|
||||
} else {
|
||||
// Remove the attribute if it exists on the server but we don't define a value for it.
|
||||
const attributeId = note.getAttribute(type, name)?.attributeId;
|
||||
if (attributeId) {
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
|
||||
* @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
|
||||
*/
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true, componentId?: string) {
|
||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||
|
||||
if (branchIdsToDelete.length === 0) {
|
||||
@@ -139,9 +139,9 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
||||
const branch = froca.getBranch(branchIdToDelete);
|
||||
|
||||
if (deleteAllClones && branch) {
|
||||
await server.remove(`notes/${branch.noteId}${query}`);
|
||||
await server.remove(`notes/${branch.noteId}${query}`, componentId);
|
||||
} else {
|
||||
await server.remove(`branches/${branchIdToDelete}${query}`);
|
||||
await server.remove(`branches/${branchIdToDelete}${query}`, componentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === "options") {
|
||||
const attributeEntity = ec.entity as FAttributeRow;
|
||||
if (attributeEntity.name === "openNoteContexts") {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
||||
loadResults.addOption(attributeEntity.name as OptionNames);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -61,9 +62,10 @@ describe("shortcuts", () => {
|
||||
});
|
||||
|
||||
describe("keyMatches", () => {
|
||||
const createKeyboardEvent = (key: string, code?: string) => ({
|
||||
const createKeyboardEvent = (key: string, code?: string, extraProps: Partial<KeyboardEvent> = {}) => ({
|
||||
key,
|
||||
code: code || `Key${key.toUpperCase()}`
|
||||
code: code || `Key${key.toUpperCase()}`,
|
||||
...extraProps
|
||||
} as KeyboardEvent);
|
||||
|
||||
it("should match regular letter keys using key code", () => {
|
||||
@@ -101,17 +103,23 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match azerty keys", () => {
|
||||
const event = createKeyboardEvent("A", "KeyQ");
|
||||
expect(keyMatches(event, "a")).toBe(true);
|
||||
expect(keyMatches(event, "q")).toBe(false);
|
||||
});
|
||||
|
||||
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA", { altKey: true });
|
||||
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
|
||||
|
||||
// Option + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH", { altKey: true });
|
||||
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
|
||||
|
||||
// Option + S produces 'ß'
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS", { altKey: true });
|
||||
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -215,6 +223,15 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("matches azerty", () => {
|
||||
const event = createKeyboardEvent({
|
||||
key: "a",
|
||||
code: "KeyQ",
|
||||
ctrlKey: true
|
||||
});
|
||||
expect(matchesShortcut(event, "Ctrl+A")).toBe(true);
|
||||
});
|
||||
|
||||
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent({
|
||||
|
||||
@@ -215,9 +215,12 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
// For letter keys, use the physical key code for consistency
|
||||
// On macOS, Option/Alt key produces special characters, so we must use e.code
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
if (e.altKey) {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
import { type AutoHighlightResult, ensureMimeTypes, highlight, highlightAuto, type HighlightResult, loadTheme, type Theme,Themes } from "@triliumnext/highlightjs";
|
||||
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { t } from "./i18n.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
import options from "./options.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { isShare } from "./utils.js";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
|
||||
let highlightingLoaded = false;
|
||||
|
||||
@@ -76,13 +77,15 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
||||
}
|
||||
|
||||
export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
if (highlightingLoaded) {
|
||||
if (!mimeTypeHint && highlightingLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load theme.
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
if (!highlightingLoaded) {
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
}
|
||||
|
||||
// Load mime types.
|
||||
let mimeTypes: MimeType[];
|
||||
@@ -94,7 +97,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
enabled: true,
|
||||
mime: mimeTypeHint.replace("-", "/")
|
||||
}
|
||||
]
|
||||
];
|
||||
} else {
|
||||
mimeTypes = mime_types.getMimeTypes();
|
||||
}
|
||||
@@ -124,9 +127,9 @@ export function isSyntaxHighlightEnabled() {
|
||||
if (!isShare) {
|
||||
const theme = options.get("codeBlockTheme");
|
||||
return !!theme && theme !== "none";
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
--row-moving-background-color: var(--accented-background-color);
|
||||
--row-text-color: var(--main-text-color);
|
||||
--row-delimiter-color: var(--more-accented-background-color);
|
||||
|
||||
|
||||
--cell-horiz-padding-size: 8px;
|
||||
--cell-vert-padding-size: 8px;
|
||||
|
||||
|
||||
--cell-editable-hover-outline-color: var(--main-border-color);
|
||||
--cell-read-only-text-color: var(--muted-text-color);
|
||||
|
||||
|
||||
--cell-editing-border-color: var(--main-border-color);
|
||||
--cell-editing-border-width: 2px;
|
||||
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
|
||||
@@ -40,10 +40,42 @@
|
||||
border-bottom: var(--col-header-bottom-border);
|
||||
background: var(--col-header-background-color);
|
||||
color: var(--col-header-text-color);
|
||||
}
|
||||
font-weight: normal;
|
||||
|
||||
.tabulator .tabulator-col-content {
|
||||
padding: 8px 4px !important;
|
||||
.tabulator-col.tabulator-range-highlight {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tabulator-col-content {
|
||||
padding: 0 !important;
|
||||
|
||||
.tabulator-col-title-holder {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
&:has(.tabulator-header-filter) {
|
||||
.tabulator-col-title-holder {
|
||||
padding: 4px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-header-filter {
|
||||
background: var(--main-background-color);
|
||||
padding: 2px 1px;
|
||||
|
||||
input {
|
||||
background: var(--main-background-color);
|
||||
color: var(--main-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -80,7 +112,6 @@
|
||||
|
||||
.tabulator-tableholder {
|
||||
padding-top: 10px;
|
||||
height: unset !important; /* Don't extend on the full height */
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
@@ -99,6 +130,14 @@
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--row-delimiter-color);
|
||||
color: var(--row-text-color);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.tabulator-range-highlight > .tabulator-cell.tabulator-frozen {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-odd {
|
||||
@@ -120,11 +159,14 @@
|
||||
margin-inline-end: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell {
|
||||
border-inline-end-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
border-inline-end-color: var(--main-border-color);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
||||
color: var(--cell-read-only-text-color);
|
||||
}
|
||||
@@ -174,10 +216,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
.tabulator-popup-container {
|
||||
@@ -192,8 +230,27 @@
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
:root .tabulator .tabulator-footer {
|
||||
border-top: unset;
|
||||
background: transparent;
|
||||
color: var(--main-text-color);
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tabulator-page {
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--hover-item-border-color);
|
||||
color: var(--button-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
background: var(--button-background-color);
|
||||
color: var(--input-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ function injectGlobals() {
|
||||
uncheckedWindow.$ = $;
|
||||
uncheckedWindow.WebSocket = () => {};
|
||||
uncheckedWindow.glob = {
|
||||
isMainWindow: true,
|
||||
windowId: "main"
|
||||
isMainWindow: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1815,7 +1815,11 @@
|
||||
"configure_launchbar": "Configure Launchbar"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "No rows have been returned for this query"
|
||||
"not_executed": "The query has not been executed yet.",
|
||||
"no_rows": "No rows have been returned for this query",
|
||||
"failed": "SQL query execution has failed",
|
||||
"statement_result": "Statement result",
|
||||
"execute_now": "Execute now"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tables"
|
||||
|
||||
1
apps/client/src/types.d.ts
vendored
@@ -36,7 +36,6 @@ interface CustomGlobals {
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
|
||||
@@ -7,7 +7,6 @@ import Component from "../components/component";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
|
||||
import froca from "../services/froca";
|
||||
import { t } from "../services/i18n";
|
||||
import { copyImageReferenceToClipboard } from "../services/image";
|
||||
@@ -101,7 +100,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
|
||||
@@ -265,9 +265,13 @@ function useNoteInfo() {
|
||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||
const [ type, setType ] = useState<ExtendedNoteType>();
|
||||
const [ mime, setMime ] = useState<string>();
|
||||
const refreshIdRef = useRef(0);
|
||||
|
||||
function refresh() {
|
||||
const refreshId = ++refreshIdRef.current;
|
||||
|
||||
getExtendedWidgetType(actualNote, noteContext).then(type => {
|
||||
if (refreshId !== refreshIdRef.current) return;
|
||||
setNote(actualNote);
|
||||
setType(type);
|
||||
setMime(actualNote?.mime);
|
||||
@@ -318,6 +322,8 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
|
||||
resultingType = "noteMap";
|
||||
} else if (type === "text" && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyText";
|
||||
} else if (note.isTriliumSqlite()) {
|
||||
resultingType = "sqlConsole";
|
||||
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyCode";
|
||||
} else if (type === "text") {
|
||||
@@ -342,9 +348,8 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
|
||||
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = noteContext.note?.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|
||||
|| noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
}
|
||||
@@ -358,8 +363,8 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
|
||||
});
|
||||
}
|
||||
|
||||
function handlePrintReport(printReport: PrintReport) {
|
||||
if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
function handlePrintReport(printReport?: PrintReport) {
|
||||
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
toast.showPersistent({
|
||||
id: "print-report",
|
||||
icon: "bx bx-collection",
|
||||
|
||||
@@ -217,6 +217,7 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
|
||||
id={inputId}
|
||||
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
|
||||
value={valueAttr.value}
|
||||
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
|
||||
placeholder={t("promoted_attributes.unset-field-placeholder")}
|
||||
data-attribute-id={valueAttr.attributeId}
|
||||
data-attribute-type={valueAttr.type}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./NoteList.css";
|
||||
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import { VNode } from "preact";
|
||||
import { Component, VNode } from "preact";
|
||||
import { lazy, Suspense } from "preact/compat";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -120,7 +120,9 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
|
||||
}
|
||||
|
||||
const ComponentToRender = viewType && props && isEnabled && (
|
||||
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
|
||||
props.media === "print"
|
||||
? ViewComponents[viewType].print ?? ViewComponents[viewType].normal
|
||||
: ViewComponents[viewType].normal
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CreateChildrenResponse } from "@triliumnext/commons";
|
||||
import server from "../../../services/server";
|
||||
import { AttributeRow, CreateChildrenResponse } from "@triliumnext/commons";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { setAttribute, setLabel } from "../../../services/attributes";
|
||||
import froca from "../../../services/froca";
|
||||
import server from "../../../services/server";
|
||||
|
||||
interface NewEventOpts {
|
||||
title: string;
|
||||
@@ -10,6 +10,7 @@ interface NewEventOpts {
|
||||
endDate?: string | null;
|
||||
startTime?: string | null;
|
||||
endTime?: string | null;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
interface ChangeEventOpts {
|
||||
@@ -17,30 +18,48 @@ interface ChangeEventOpts {
|
||||
endDate?: string | null;
|
||||
startTime?: string | null;
|
||||
endTime?: string | null;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
export async function newEvent(parentNote: FNote, { title, startDate, endDate, startTime, endTime }: NewEventOpts) {
|
||||
// Create the note.
|
||||
const { note } = await server.post<CreateChildrenResponse>(`notes/${parentNote.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
export async function newEvent(parentNote: FNote, { title, startDate, endDate, startTime, endTime, componentId }: NewEventOpts) {
|
||||
const attributes: Omit<AttributeRow, "noteId" | "attributeId">[] = [];
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "startDate",
|
||||
value: startDate
|
||||
});
|
||||
|
||||
// Set the attributes.
|
||||
setLabel(note.noteId, "startDate", startDate);
|
||||
if (endDate) {
|
||||
setLabel(note.noteId, "endDate", endDate);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "endDate",
|
||||
value: endDate
|
||||
});
|
||||
}
|
||||
if (startTime) {
|
||||
setLabel(note.noteId, "startTime", startTime);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "startTime",
|
||||
value: startTime
|
||||
});
|
||||
}
|
||||
if (endTime) {
|
||||
setLabel(note.noteId, "endTime", endTime);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "endTime",
|
||||
value: endTime
|
||||
});
|
||||
}
|
||||
|
||||
// Create the note.
|
||||
await server.post<CreateChildrenResponse>(`notes/${parentNote.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text",
|
||||
attributes
|
||||
}, componentId);
|
||||
}
|
||||
|
||||
export async function changeEvent(note: FNote, { startDate, endDate, startTime, endTime }: ChangeEventOpts) {
|
||||
export async function changeEvent(note: FNote, { startDate, endDate, startTime, endTime, componentId }: ChangeEventOpts) {
|
||||
// Don't store the end date if it's empty.
|
||||
if (endDate === startDate) {
|
||||
endDate = undefined;
|
||||
@@ -52,12 +71,12 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
|
||||
let endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endDate").shift()?.value||"endDate";
|
||||
|
||||
const noteId = note.noteId;
|
||||
setLabel(noteId, startAttribute, startDate);
|
||||
setAttribute(note, "label", endAttribute, endDate);
|
||||
setLabel(noteId, startAttribute, startDate, false, componentId);
|
||||
setAttribute(note, "label", endAttribute, endDate, componentId);
|
||||
|
||||
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
|
||||
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
|
||||
|
||||
setAttribute(note, "label", startAttribute, startTime);
|
||||
setAttribute(note, "label", endAttribute, endTime);
|
||||
setAttribute(note, "label", startAttribute, startTime, componentId);
|
||||
setAttribute(note, "label", endAttribute, endTime, componentId);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import branches from "../../../services/branches";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote) {
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote, componentId?: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -30,16 +30,16 @@ export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parent
|
||||
}
|
||||
|
||||
if (branchIdToDelete) {
|
||||
await branches.deleteNotes([ branchIdToDelete ], false, false);
|
||||
await branches.deleteNotes([ branchIdToDelete ], false, false, componentId);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
kind: "custom",
|
||||
componentFn: () => NoteColorPicker({note: note})
|
||||
componentFn: () => NoteColorPicker({note})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, note.noteId),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
|
||||
import froca from "../../../services/froca";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import server from "../../../services/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import froca from "../../../services/froca";
|
||||
import server from "../../../services/server";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
|
||||
interface Event {
|
||||
startDate: string,
|
||||
endDate?: string | null,
|
||||
@@ -105,7 +106,8 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
|
||||
endDate = (endTime ? `${endDate}T${endTime}:00` : endDate);
|
||||
const eventData: EventInput = {
|
||||
title: title,
|
||||
id: note.noteId,
|
||||
title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}?popup`,
|
||||
noteId: note.noteId,
|
||||
@@ -148,12 +150,12 @@ async function parseCustomTitle(customTitlettributeName: string | null, note: FN
|
||||
}
|
||||
|
||||
async function buildDisplayedAttributes(note: FNote, calendarDisplayedAttributes: string[]) {
|
||||
const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name))
|
||||
const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name));
|
||||
const result: Array<[string, string]> = [];
|
||||
|
||||
for (const attribute of filteredDisplayedAttributes) {
|
||||
if (attribute.type === "label") result.push([attribute.name, attribute.value]);
|
||||
else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""])
|
||||
else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""]);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, Local
|
||||
import { DateClickArg } from "@fullcalendar/interaction";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
import { RefObject } from "preact";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
@@ -17,6 +17,7 @@ import { isMobile } from "../../../services/utils";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
@@ -87,6 +88,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
};
|
||||
|
||||
export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarViewData>) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = useRef<FullCalendar>(null);
|
||||
|
||||
@@ -105,26 +107,34 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const eventBuilder = useMemo(() => {
|
||||
if (!isCalendarRoot) {
|
||||
return async () => await buildEvents(noteIds);
|
||||
}
|
||||
}
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
|
||||
}, [isCalendarRoot, noteIds]);
|
||||
|
||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||
const locale = useLocale();
|
||||
|
||||
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||
const { eventDidMount } = useEventDisplayCustomization(note, parentComponent?.componentId);
|
||||
const editingProps = useEditing(note, isEditable, isCalendarRoot, parentComponent?.componentId);
|
||||
|
||||
// React to changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) // note title change.
|
||||
|| loadResults.getAttributeRows().some((a) => noteIds.includes(a.noteId ?? ""))) // subnote change.
|
||||
{
|
||||
const api = calendarRef.current;
|
||||
if (!api) return;
|
||||
|
||||
// Subnote attribute change.
|
||||
if (loadResults.getAttributeRows(parentComponent?.componentId).some((a) => noteIds.includes(a.noteId ?? ""))) {
|
||||
// Defer execution after the load results are processed so that the event builder has the updated data to work with.
|
||||
setTimeout(() => {
|
||||
calendarRef.current?.refetchEvents();
|
||||
}, 0);
|
||||
setTimeout(() => api.refetchEvents(), 0);
|
||||
return; // early return since we'll refresh the events anyway
|
||||
}
|
||||
|
||||
// Title change.
|
||||
for (const noteId of loadResults.getNoteIds().filter(noteId => noteIds.includes(noteId))) {
|
||||
const event = api.getEventById(noteId);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
if (!event || !note) continue;
|
||||
event.setProp("title", note.title);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,7 +232,7 @@ function useLocale() {
|
||||
return calendarLocale;
|
||||
}
|
||||
|
||||
function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean, componentId: string | undefined) {
|
||||
const onCalendarSelection = useCallback(async (e: DateSelectArg) => {
|
||||
const { startDate, endDate } = parseStartEndDateFromEvent(e);
|
||||
if (!startDate) return;
|
||||
@@ -234,8 +244,8 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
return;
|
||||
}
|
||||
|
||||
newEvent(note, { title, startDate, endDate, startTime, endTime });
|
||||
}, [ note ]);
|
||||
newEvent(note, { title, startDate, endDate, startTime, endTime, componentId });
|
||||
}, [ note, componentId ]);
|
||||
|
||||
const onEventChange = useCallback(async (e: EventChangeArg) => {
|
||||
const { startDate, endDate } = parseStartEndDateFromEvent(e.event);
|
||||
@@ -244,8 +254,8 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
const { startTime, endTime } = parseStartEndTimeFromEvent(e.event);
|
||||
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||
if (!note) return;
|
||||
changeEvent(note, { startDate, endDate, startTime, endTime });
|
||||
}, []);
|
||||
changeEvent(note, { startDate, endDate, startTime, endTime, componentId });
|
||||
}, [ componentId ]);
|
||||
|
||||
// Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root.
|
||||
const onDateClick = useCallback(async (e: DateClickArg) => {
|
||||
@@ -264,7 +274,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function useEventDisplayCustomization(parentNote: FNote) {
|
||||
function useEventDisplayCustomization(parentNote: FNote, componentId: string | undefined) {
|
||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||
|
||||
@@ -321,7 +331,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
||||
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||
if (!note) return;
|
||||
|
||||
openCalendarContextMenu(contextMenuEvent, note, parentNote);
|
||||
openCalendarContextMenu(contextMenuEvent, note, parentNote, componentId);
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
|
||||
.tabulator-tableholder {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-view-container {
|
||||
@@ -68,4 +72,4 @@
|
||||
inset-inline-start: 0;
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
|
||||
import "tabulator-tables/dist/css/tabulator.css";
|
||||
import "../../../../src/stylesheets/table.css";
|
||||
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
|
||||
import { isValidElement, RefObject } from "preact";
|
||||
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
|
||||
|
||||
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
|
||||
|
||||
interface TableProps<T> extends Omit<Options, "data" | "footerElement" | "index"> {
|
||||
tabulatorRef: RefObject<VanillaTabulator>;
|
||||
tabulatorRef?: RefObject<VanillaTabulator>;
|
||||
className?: string;
|
||||
data?: T[];
|
||||
modules?: (new (table: VanillaTabulator) => Module)[];
|
||||
events?: Partial<EventCallBackMethods>;
|
||||
index: keyof T;
|
||||
index?: keyof T;
|
||||
footerElement?: string | HTMLElement | JSX.Element;
|
||||
onReady?: () => void;
|
||||
}
|
||||
@@ -43,7 +45,9 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
|
||||
|
||||
tabulator.on("tableBuilt", () => {
|
||||
tabulatorRef.current = tabulator;
|
||||
externalTabulatorRef.current = tabulator;
|
||||
if (externalTabulatorRef) {
|
||||
externalTabulatorRef.current = tabulator;
|
||||
}
|
||||
onReady?.();
|
||||
});
|
||||
|
||||
@@ -62,12 +66,15 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
|
||||
for (const [ eventName, handler ] of Object.entries(events)) {
|
||||
tabulator.off(eventName as keyof EventCallBackMethods, handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, Object.values(events ?? {}));
|
||||
|
||||
// Change in data.
|
||||
useEffect(() => { tabulatorRef.current?.setData(data) }, [ data ]);
|
||||
useEffect(() => { columns && tabulatorRef.current?.setColumns(columns)}, [ data]);
|
||||
useEffect(() => { tabulatorRef.current?.setData(data); }, [ data ]);
|
||||
useEffect(() => {
|
||||
if (!columns) return;
|
||||
tabulatorRef.current?.setColumns(columns);
|
||||
}, [ columns ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className} />
|
||||
|
||||
@@ -82,6 +82,10 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail.full-height {
|
||||
flex-grow: 0;
|
||||
height: 100%;
|
||||
@@ -106,4 +110,4 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ComponentChild } from "preact";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import NoteIcon from "../note_icon";
|
||||
@@ -22,12 +23,12 @@ const supportedNoteTypes = new Set<NoteType>([
|
||||
export default function InlineTitle() {
|
||||
const { note, parentComponent, viewScope } = useNoteContext();
|
||||
const type = useNoteProperty(note, "type");
|
||||
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
|
||||
const [ shown, setShown ] = useState(shouldShow(note, type, viewScope));
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ titleHidden, setTitleHidden ] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setShown(shouldShow(note?.noteId, type, viewScope));
|
||||
setShown(shouldShow(note, type, viewScope));
|
||||
}, [ note, type, viewScope ]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -69,9 +70,10 @@ export default function InlineTitle() {
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
function shouldShow(note: FNote | null | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
if (viewScope?.viewMode !== "default") return false;
|
||||
if (noteId?.startsWith("_options")) return true;
|
||||
if (note?.noteId?.startsWith("_options")) return true;
|
||||
if (note?.isTriliumSqlite()) return false;
|
||||
return type && supportedNoteTypes.has(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function NoteTypeSwitcher() {
|
||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() &&
|
||||
<div
|
||||
className="note-type-switcher"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./StatusBar.css";
|
||||
|
||||
import { Locale, NoteType } from "@triliumnext/commons";
|
||||
import { Locale, NOTE_TYPE_ICONS, NoteType } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren, RefObject } from "preact";
|
||||
@@ -9,7 +9,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "p
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote, { NOTE_TYPE_ICONS } from "../../entities/fnote";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
|
||||
@@ -1232,7 +1232,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
refreshCtx.noteIdsToUpdate.add(noteId);
|
||||
}
|
||||
|
||||
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
|
||||
const hasNotesToUpdateOrReload = refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0;
|
||||
const hasNoteReorderingChange = loadResults.getNoteReorderings().length > 0;
|
||||
if (hasNotesToUpdateOrReload || hasNoteReorderingChange) {
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
}
|
||||
|
||||
@@ -1393,6 +1395,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
for (const parentNoteId of loadResults.getNoteReorderings()) {
|
||||
for (const node of this.getNodesByNoteId(parentNoteId)) {
|
||||
console.log("Reordering ", node);
|
||||
if (node.isLoaded()) {
|
||||
this.sortChildren(node);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
|
||||
* for protected session or attachment information.
|
||||
*/
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat" | "sqlConsole";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
@@ -140,5 +140,10 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
view: () => import("./type_widgets/AiChat"),
|
||||
className: "ai-chat-widget-container",
|
||||
isFullHeight: true
|
||||
},
|
||||
sqlConsole: {
|
||||
view: () => import("./type_widgets/SqlConsole"),
|
||||
className: "sql-console-widget-container",
|
||||
isFullHeight: true
|
||||
}
|
||||
};
|
||||
|
||||
18
apps/client/src/widgets/react/NoItems.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.no-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.75em;
|
||||
color: var(--muted-text-color);
|
||||
height: 100%;
|
||||
|
||||
.tn-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
21
apps/client/src/widgets/react/NoItems.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import "./NoItems.css";
|
||||
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface NoItemsProps {
|
||||
icon: string;
|
||||
text: string;
|
||||
children?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function NoItems({ icon, text, children }: NoItemsProps) {
|
||||
return (
|
||||
<div className="no-items">
|
||||
<Icon icon={icon} />
|
||||
{text}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -184,7 +184,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <ActionButton
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
|
||||
export default function ScrollPadding() {
|
||||
const { note, parentComponent, ntxId, viewScope } = useNoteContext();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number>(10);
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default";
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "")
|
||||
&& viewScope?.viewMode === "default"
|
||||
&& !note?.isTriliumSqlite();
|
||||
|
||||
const refreshHeight = () => {
|
||||
if (!ref.current) return;
|
||||
@@ -37,6 +40,6 @@ export default function ScrollPadding() {
|
||||
style={{ height }}
|
||||
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
|
||||
/>
|
||||
: <div></div>
|
||||
)
|
||||
: <div />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,22 +40,4 @@ body.experimental-feature-new-layout #right-pane {
|
||||
.gutter-vertical + .card .card-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.75em;
|
||||
color: var(--muted-text-color);
|
||||
|
||||
.tn-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { VNode } from "preact";
|
||||
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { WidgetsByParent } from "../../services/bundle";
|
||||
@@ -12,7 +12,7 @@ import options from "../../services/options";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
|
||||
import Button from "../react/Button";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import NoItems from "../react/NoItems";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import PdfAttachments from "./pdf/PdfAttachments";
|
||||
@@ -47,14 +47,15 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
items.length > 0 ? (
|
||||
items
|
||||
) : (
|
||||
<div className="no-items">
|
||||
<Icon icon="bx bx-sidebar" />
|
||||
{t("right_pane.empty_message")}
|
||||
<NoItems
|
||||
icon="bx bx-sidebar"
|
||||
text={t("right_pane.empty_message")}
|
||||
>
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
</div>
|
||||
</NoItems>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.sql-result-widget {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-console-result-container td {
|
||||
white-space: preserve;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import "./sql_result.css";
|
||||
import { useState } from "preact/hooks";
|
||||
import Alert from "./react/Alert";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
export default function SqlResults() {
|
||||
const { note, ntxId } = useNoteContext();
|
||||
const [ results, setResults ] = useState<SqlExecuteResults>();
|
||||
|
||||
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
setResults(results);
|
||||
})
|
||||
|
||||
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium";
|
||||
return (
|
||||
<div className={`sql-result-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
|
||||
<Alert type="info">
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
|
||||
}
|
||||
|
||||
// selects
|
||||
return <SqlResultTable rows={rows} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||
if (!rows.length) return;
|
||||
|
||||
return (
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr>
|
||||
{Object.values(row).map(cell => <td>{cell}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-inline-end: 10%;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas > .dropdown {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas button.btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sql-console-result-container {
|
||||
width: 100%;
|
||||
font-size: smaller;
|
||||
margin-top: 10px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.dropdown .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
import "./sql_table_schemas.css";
|
||||
import { SchemaResponse } from "@triliumnext/commons";
|
||||
import server from "../services/server";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
|
||||
export default function SqlTableSchemas() {
|
||||
const { note } = useNoteContext();
|
||||
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||
}, []);
|
||||
|
||||
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
|
||||
return (
|
||||
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
{t("sql_table_schemas.tables")}{": "}
|
||||
|
||||
<span class="sql-table-schemas">
|
||||
{schemas.map(({ name, columns }) => (
|
||||
<>
|
||||
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
|
||||
>
|
||||
<table className="table-schema">
|
||||
{columns.map(column => (
|
||||
<tr>
|
||||
<td>{column.name}</td>
|
||||
<td>{column.type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Dropdown>
|
||||
{" "}
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import render from "../../services/render";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import Alert from "../react/Alert";
|
||||
import "./Render.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import render from "../../services/render";
|
||||
import Alert from "../react/Alert";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -31,6 +34,13 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
refresh();
|
||||
});
|
||||
|
||||
// Refresh on attribute change.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(a => a.type === "relation" && a.name === "renderNote" && attributes.isAffecting(a, note))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Integration with search.
|
||||
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
|
||||
81
apps/client/src/widgets/type_widgets/SqlConsole.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.sql-console-widget-container {
|
||||
.note-detail-split.split-vertical {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.note-detail-split-preview {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
}
|
||||
|
||||
.sql-result-widget {
|
||||
height: 100%;
|
||||
|
||||
> .sql-console-result-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: smaller;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
|
||||
> .tabulator {
|
||||
--cell-vert-padding-size: 4px;
|
||||
|
||||
> .tabulator-tableholder {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> .tabulator-footer,
|
||||
> .tabulator-footer .tabulator-footer-contents {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-inline-end: 10%;
|
||||
contain: none !important;
|
||||
|
||||
.sql-table-schemas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
> .dropdown {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.dropdown .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
apps/client/src/widgets/type_widgets/SqlConsole.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import "./SqlConsole.css";
|
||||
|
||||
import { SchemaResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { ClipboardModule, EditModule, ExportModule, FilterModule, FormatModule, FrozenColumnsModule, KeybindingsModule, PageModule, ResizeColumnsModule, SelectRangeModule, SelectRowModule, SortModule } from "tabulator-tables";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import Tabulator from "../collections/table/tabulator";
|
||||
import Button from "../react/Button";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import NoItems from "../react/NoItems";
|
||||
import SplitEditor from "./helpers/SplitEditor";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function SqlConsole(props: TypeWidgetProps) {
|
||||
return (
|
||||
<SplitEditor
|
||||
noteType="code"
|
||||
{...props}
|
||||
editorBefore={<SqlTableSchemas {...props} />}
|
||||
previewContent={<SqlResults key={props.note.noteId} {...props} />}
|
||||
forceOrientation="vertical"
|
||||
splitOptions={{
|
||||
sizes: [ 70, 30 ]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SqlResults({ ntxId }: TypeWidgetProps) {
|
||||
const [ response, setResponse ] = useState<SqlExecuteResponse>();
|
||||
|
||||
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, response }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
setResponse(response);
|
||||
});
|
||||
|
||||
// Not yet executed.
|
||||
if (response === undefined) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-data"
|
||||
text={t("sql_result.not_executed")}
|
||||
>
|
||||
<Button
|
||||
text={t("sql_result.execute_now")}
|
||||
triggerCommand="runActiveNote"
|
||||
/>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// Executed but failed.
|
||||
if (response && !response.success) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-error"
|
||||
text={t("sql_result.failed")}
|
||||
>
|
||||
<pre className="sql-error-message selectable-text">{response.error}</pre>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// Zero results.
|
||||
if (response?.results.length === 1 && Array.isArray(response.results[0]) && response.results[0].length === 0) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-rectangle"
|
||||
text={t("sql_result.no_rows")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sql-result-widget">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{response?.results.map((rows, index) => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
return (
|
||||
<NoItems
|
||||
key={index}
|
||||
icon="bx bx-play"
|
||||
text={t("sql_result.statement_result")}
|
||||
>
|
||||
<pre key={index}>{JSON.stringify(rows, null, "\t")}</pre>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// selects
|
||||
return <SqlResultTable key={index} rows={rows} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||
if (!rows.length) return;
|
||||
|
||||
return (
|
||||
<Tabulator
|
||||
layout="fitDataFill"
|
||||
modules={[ ResizeColumnsModule, SortModule, SelectRangeModule, ClipboardModule, KeybindingsModule, EditModule, ExportModule, SelectRowModule, FormatModule, FrozenColumnsModule, FilterModule, PageModule ]}
|
||||
selectableRange
|
||||
clipboard="copy"
|
||||
clipboardCopyRowRange="range"
|
||||
clipboardCopyConfig={{
|
||||
rowHeaders: false,
|
||||
columnHeaders: false
|
||||
}}
|
||||
pagination
|
||||
paginationSize={15}
|
||||
paginationSizeSelector
|
||||
paginationCounter="rows"
|
||||
height="100%"
|
||||
columns={[
|
||||
{
|
||||
title: "#",
|
||||
formatter: "rownum",
|
||||
width: 60,
|
||||
hozAlign: "right",
|
||||
frozen: true
|
||||
},
|
||||
...Object.keys(rows[0]).map(key => ({
|
||||
title: key,
|
||||
field: key,
|
||||
width: 250,
|
||||
minWidth: 100,
|
||||
widthGrow: 1,
|
||||
resizable: true,
|
||||
headerFilter: true as const
|
||||
}))
|
||||
]}
|
||||
data={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SqlTableSchemas({ note }: TypeWidgetProps) {
|
||||
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||
}, []);
|
||||
|
||||
const isEnabled = note.isTriliumSqlite() && schemas;
|
||||
return (
|
||||
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
{t("sql_table_schemas.tables")}{": "}
|
||||
|
||||
<span class="sql-table-schemas">
|
||||
{schemas.map(({ name, columns }) => (
|
||||
<Dropdown key={name} text={name} noSelectButtonStyle hideToggleArrow>
|
||||
<table className="table-schema">
|
||||
{columns.map(column => (
|
||||
<tr key={column.name}>
|
||||
<td>{column.name}</td>
|
||||
<td>{column.type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Dropdown>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export interface CanvasContent {
|
||||
appState: Partial<AppState>;
|
||||
}
|
||||
|
||||
/** Subset of the app state that should be persisted whenever they change. This explicitly excludes transient state like the current selection or zoom level. */
|
||||
type ImportantAppState = Pick<AppState, "gridModeEnabled" | "viewBackgroundColor">;
|
||||
|
||||
export default function useCanvasPersistence(note: FNote, noteContext: NoteContext | null | undefined, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"], isReadOnly: boolean): Partial<ExcalidrawProps> {
|
||||
const libraryChanged = useRef(false);
|
||||
|
||||
@@ -37,6 +40,8 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
const libraryCache = useRef<LibraryItem[]>([]);
|
||||
const attachmentMetadata = useRef<AttachmentMetadata[]>([]);
|
||||
|
||||
const appStateToCompare = useRef<Partial<ImportantAppState>>({});
|
||||
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteContext,
|
||||
@@ -47,7 +52,6 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
|
||||
libraryCache.current = [];
|
||||
attachmentMetadata.current = [];
|
||||
currentSceneVersion.current = -1;
|
||||
|
||||
// load saved content into excalidraw canvas
|
||||
let content: CanvasContent = {
|
||||
@@ -65,6 +69,9 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
|
||||
loadData(api, content, theme);
|
||||
|
||||
// Initialize tracking state after loading to prevent redundant updates from initial onChange events
|
||||
currentSceneVersion.current = getSceneVersion(api.getSceneElements());
|
||||
|
||||
// load the library state
|
||||
loadLibrary(note).then(({ libraryItems, metadata }) => {
|
||||
// Update the library and save to independent variables
|
||||
@@ -78,7 +85,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
async getData() {
|
||||
const api = apiRef.current;
|
||||
if (!api) return;
|
||||
const { content, svg } = await getData(api);
|
||||
const { content, svg } = await getData(api, appStateToCompare);
|
||||
const attachments: SavedData["attachments"] = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
|
||||
|
||||
// libraryChanged is unset in dataSaved()
|
||||
@@ -149,21 +156,47 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
const oldSceneVersion = currentSceneVersion.current;
|
||||
const newSceneVersion = getSceneVersion(apiRef.current.getSceneElements());
|
||||
|
||||
if (newSceneVersion !== oldSceneVersion) {
|
||||
let hasChanges = (newSceneVersion !== oldSceneVersion);
|
||||
|
||||
// There are cases where the scene version does not change, but appState did.
|
||||
if (!hasChanges) {
|
||||
const importantAppState = appStateToCompare.current;
|
||||
const currentAppState = apiRef.current.getAppState();
|
||||
for (const key in importantAppState) {
|
||||
if (importantAppState[key as keyof ImportantAppState] !== currentAppState[key as keyof ImportantAppState]) {
|
||||
hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
currentSceneVersion.current = newSceneVersion;
|
||||
}
|
||||
},
|
||||
onLibraryChange: () => {
|
||||
libraryChanged.current = true;
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
onLibraryChange: (libraryItems) => {
|
||||
if (!apiRef.current || isReadOnly) return;
|
||||
|
||||
// Check if library actually changed by comparing with cached state
|
||||
const hasChanges =
|
||||
libraryItems.length !== libraryCache.current.length ||
|
||||
libraryItems.some(item => {
|
||||
const cachedItem = libraryCache.current.find(cached => cached.id === item.id);
|
||||
return !cachedItem || cachedItem.name !== item.name;
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
libraryChanged.current = true;
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getData(api: ExcalidrawImperativeAPI) {
|
||||
async function getData(api: ExcalidrawImperativeAPI, appStateToCompare: RefObject<Partial<ImportantAppState>>) {
|
||||
const elements = api.getSceneElements();
|
||||
const appState = api.getAppState();
|
||||
|
||||
@@ -188,6 +221,12 @@ async function getData(api: ExcalidrawImperativeAPI) {
|
||||
}
|
||||
});
|
||||
|
||||
const importantAppState: ImportantAppState = {
|
||||
gridModeEnabled: appState.gridModeEnabled,
|
||||
viewBackgroundColor: appState.viewBackgroundColor
|
||||
};
|
||||
appStateToCompare.current = importantAppState;
|
||||
|
||||
const content = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -197,7 +236,7 @@ async function getData(api: ExcalidrawImperativeAPI) {
|
||||
scrollX: appState.scrollX,
|
||||
scrollY: appState.scrollY,
|
||||
zoom: appState.zoom,
|
||||
gridModeEnabled: appState.gridModeEnabled
|
||||
...importantAppState
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import utils, { isMobile } from "../../../services/utils";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import "./SplitEditor.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import utils, { isMobile } from "../../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import { useNoteBlob, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
className?: string;
|
||||
@@ -15,6 +17,8 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
splitOptions?: Split.Options;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
editorBefore?: ComponentChildren;
|
||||
forceOrientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,13 +30,24 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
* - Can display errors to the user via {@link setError}.
|
||||
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
|
||||
*/
|
||||
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, ...editorProps }: SplitEditorProps) {
|
||||
const splitEditorOrientation = useSplitOrientation();
|
||||
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export default function SplitEditor(props: SplitEditorProps) {
|
||||
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
|
||||
|
||||
const editor = (!readOnly &&
|
||||
if (readOnly) {
|
||||
return <ReadOnlyView {...props} />;
|
||||
}
|
||||
|
||||
return <EditorWithSplit {...props} />;
|
||||
|
||||
}
|
||||
|
||||
function EditorWithSplit({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
|
||||
const editor = (
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
<EditableCode
|
||||
note={note}
|
||||
@@ -48,19 +63,14 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
</div>
|
||||
);
|
||||
|
||||
const preview = (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const preview = <PreviewContainer
|
||||
error={error}
|
||||
previewContent={previewContent}
|
||||
previewButtons={previewButtons}
|
||||
/>;
|
||||
|
||||
useEffect(() => {
|
||||
if (!utils.isDesktop() || !containerRef.current || readOnly) return;
|
||||
if (!utils.isDesktop() || !containerRef.current) return;
|
||||
const elements = Array.from(containerRef.current?.children) as HTMLElement[];
|
||||
const splitInstance = Split(elements, {
|
||||
rtl: glob.isRtl,
|
||||
@@ -71,15 +81,52 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
});
|
||||
|
||||
return () => splitInstance.destroy();
|
||||
}, [ readOnly, splitEditorOrientation ]);
|
||||
}, [ splitEditorOrientation ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${"split-" + splitEditorOrientation} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyView({ ...props }: SplitEditorProps) {
|
||||
const { note, onContentChanged } = props;
|
||||
const content = useNoteBlob(note);
|
||||
const onContentChangedRef = useRef(onContentChanged);
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current = onContentChanged;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current?.(content?.content ?? "");
|
||||
}, [ content ]);
|
||||
|
||||
return (
|
||||
<div className={`note-detail-split note-detail-printable ${props.className} split-read-only`}>
|
||||
<PreviewContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewContainer({ error, previewContent, previewButtons }: {
|
||||
error?: string | null;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
}) {
|
||||
return (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
@@ -88,11 +135,12 @@ export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
className="tn-tool-button"
|
||||
noIconActionClass
|
||||
titlePosition="top"
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
|
||||
function useSplitOrientation() {
|
||||
function useSplitOrientation(forceOrientation?: "horizontal" | "vertical") {
|
||||
const [ splitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
if (forceOrientation) return forceOrientation;
|
||||
if (isMobile()) return "vertical";
|
||||
if (!splitEditorOrientation) return "horizontal";
|
||||
return splitEditorOrientation as "horizontal" | "vertical";
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import server from "../../../services/server";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
import { RefObject } from "preact";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import utils from "../../../services/utils";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import utils from "../../../services/utils";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
|
||||
interface SvgSplitEditorProps extends Omit<SplitEditorProps, "previewContent"> {
|
||||
/**
|
||||
@@ -144,7 +145,7 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg: string | undefined) {
|
||||
@@ -181,7 +182,7 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
|
||||
lastPanZoom.current = {
|
||||
pan: zoomInstance.getPan(),
|
||||
zoom: zoomInstance.getZoom()
|
||||
}
|
||||
};
|
||||
zoomRef.current = undefined;
|
||||
zoomInstance.destroy();
|
||||
};
|
||||
|
||||
@@ -191,7 +191,6 @@ function ExperimentalOptions() {
|
||||
values={filteredExperimentalFeatures}
|
||||
keyProperty="id"
|
||||
titleProperty="name"
|
||||
descriptionProperty="description"
|
||||
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
|
||||
/>
|
||||
</OptionsSection>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import FormCheckbox from "../../../react/FormCheckbox";
|
||||
|
||||
interface CheckboxListProps<T> {
|
||||
values: T[];
|
||||
keyProperty: keyof T;
|
||||
titleProperty?: keyof T;
|
||||
disabledProperty?: keyof T;
|
||||
descriptionProperty?: keyof T;
|
||||
currentValue: string[];
|
||||
onChange: (newValues: string[]) => void;
|
||||
columnWidth?: string;
|
||||
}
|
||||
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
function toggleValue(value: string) {
|
||||
if (currentValue.includes(value)) {
|
||||
// Already there, needs removing.
|
||||
@@ -25,17 +22,20 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
|
||||
return (
|
||||
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
||||
{values.map(value => (
|
||||
<li key={String(value[keyProperty])}>
|
||||
<FormCheckbox
|
||||
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
name={String(value[keyProperty])}
|
||||
currentValue={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
|
||||
onChange={() => toggleValue(String(value[keyProperty]))}
|
||||
/>
|
||||
<li>
|
||||
<label className="tn-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={String(value[keyProperty])}
|
||||
checked={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,15 @@ export default defineConfig(() => ({
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
entryFileNames: (chunk) => {
|
||||
// We enforce a hash in the main index file to avoid caching issues, this only works because we have the HTML entry point.
|
||||
if (chunk.name === "index") {
|
||||
return "src/[name]-[hash].js";
|
||||
}
|
||||
|
||||
// For EJS-rendered pages (e.g. login) we need to have a stable name.
|
||||
return "src/[name].js";
|
||||
},
|
||||
chunkFileNames: "src/[name]-[hash].js",
|
||||
assetFileNames: "src/[name]-[hash].[ext]",
|
||||
manualChunks: {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-dl": "4.0.0",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.7",
|
||||
"electron": "40.0.0",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 727 B |
|
Before Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 931 B |
|
Before Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 492 B |
@@ -6,7 +6,6 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import tray from "@triliumnext/server/src/services/tray.js";
|
||||
import options from "@triliumnext/server/src/services/options.js";
|
||||
|
||||
import electronDebug from "electron-debug";
|
||||
import electronDl from "electron-dl";
|
||||
import { PRODUCT_NAME } from "./app-info";
|
||||
@@ -70,12 +69,10 @@ async function main() {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("second-instance", async (event, commandLine) => {
|
||||
app.on("second-instance", (event, commandLine) => {
|
||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||
if (commandLine.includes("--new-window")) {
|
||||
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
|
||||
const extraWindowId = randomString(4);
|
||||
windowService.createExtraWindow(extraWindowId, "");
|
||||
windowService.createExtraWindow("");
|
||||
} else if (lastFocusedWindow) {
|
||||
if (lastFocusedWindow.isMinimized()) {
|
||||
lastFocusedWindow.restore();
|
||||
@@ -127,8 +124,7 @@ async function onReady() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await normalizeOpenNoteContexts();
|
||||
|
||||
tray.createTray();
|
||||
} else {
|
||||
await windowService.createSetupWindow();
|
||||
@@ -137,30 +133,6 @@ async function onReady() {
|
||||
await windowService.registerGlobalShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts.
|
||||
* This function normalizes those timestamps to the current time for correct sorting/filtering.
|
||||
*/
|
||||
async function normalizeOpenNoteContexts() {
|
||||
const savedWindows = options.getOptionJson("openNoteContexts") || [];
|
||||
const now = Date.now();
|
||||
|
||||
let changed = false;
|
||||
for (const win of savedWindows) {
|
||||
if (win.windowId !== "main" && win.closedAt === 0) {
|
||||
win.closedAt = now;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
const { default: cls } = (await import("@triliumnext/server/src/services/cls.js"));
|
||||
cls.wrap(() => {
|
||||
options.setOption("openNoteContexts", JSON.stringify(savedWindows));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronLocale() {
|
||||
const uiLocale = options.getOptionOrNull("locale");
|
||||
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"mime-types": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"tsx": "4.21.0",
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
|
||||
"dependencies": {
|
||||
"archiver": "7.0.1",
|
||||
"better-sqlite3": "12.6.0"
|
||||
"better-sqlite3": "12.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@triliumnext/client": "workspace:*",
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.7",
|
||||
"electron": "40.0.0",
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -185,6 +185,9 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
return components.join("/");
|
||||
});
|
||||
|
||||
// Remove data-list-item-id created by CKEditor for lists
|
||||
content = content.replace(/ data-list-item-id="[^"]*"/g, "");
|
||||
|
||||
return content;
|
||||
|
||||
function findAttachment(targetAttachmentId: string) {
|
||||
|
||||
@@ -43,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Highlights list");
|
||||
|
||||
await expect(app.sidebar).toContainText(/highlights/i);
|
||||
await expect(app.sidebar).toContainText("10 highlights");
|
||||
const rootList = app.sidebar.locator(".highlights-list ol");
|
||||
let index = 0;
|
||||
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class App {
|
||||
|
||||
// Wait for the page to load.
|
||||
if (url === "/") {
|
||||
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-bullseye-slim AS builder
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-bullseye-slim
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-alpine AS builder
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-alpine
|
||||
FROM node:24.13.0-alpine
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-alpine AS builder
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-alpine
|
||||
FROM node:24.13.0-alpine
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-bullseye-slim AS builder
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-bullseye-slim
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0"
|
||||
"better-sqlite3": "12.6.2"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"html-to-text": "9.0.5",
|
||||
"node-html-parser": "7.0.2",
|
||||
"sucrase": "3.35.1"
|
||||
@@ -74,7 +74,7 @@
|
||||
"bindings": "1.5.0",
|
||||
"bootstrap": "5.3.8",
|
||||
"chardet": "2.1.1",
|
||||
"cheerio": "1.1.2",
|
||||
"cheerio": "1.2.0",
|
||||
"chokidar": "5.0.0",
|
||||
"cls-hooked": "4.2.2",
|
||||
"compression": "1.8.1",
|
||||
@@ -82,8 +82,8 @@
|
||||
"csrf-csrf": "3.2.2",
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "3.1.10",
|
||||
"electron": "39.2.7",
|
||||
"ejs": "4.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -91,7 +91,7 @@
|
||||
"express-http-proxy": "2.1.2",
|
||||
"express-openid-connect": "2.19.4",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-session": "1.18.2",
|
||||
"express-session": "1.19.0",
|
||||
"file-uri-to-path": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"helmet": "8.1.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turndown": "7.2.2",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import favicon from "serve-favicon";
|
||||
import cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import config from "./services/config.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
import assets from "./routes/assets.js";
|
||||
import routes from "./routes/routes.js";
|
||||
import custom from "./routes/custom.js";
|
||||
import error_handlers from "./routes/error_handlers.js";
|
||||
import { startScheduledCleanup } from "./services/erase.js";
|
||||
import sql_init from "./services/sql_init.js";
|
||||
import { auth } from "express-openid-connect";
|
||||
import openID from "./services/open_id.js";
|
||||
import { t } from "i18next";
|
||||
import eventService from "./services/events.js";
|
||||
import log from "./services/log.js";
|
||||
import "./services/handlers.js";
|
||||
import "./becca/becca_loader.js";
|
||||
|
||||
import compression from "compression";
|
||||
import cookieParser from "cookie-parser";
|
||||
import ejs from "ejs";
|
||||
import express from "express";
|
||||
import { auth } from "express-openid-connect";
|
||||
import helmet from "helmet";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
import favicon from "serve-favicon";
|
||||
|
||||
import assets from "./routes/assets.js";
|
||||
import custom from "./routes/custom.js";
|
||||
import error_handlers from "./routes/error_handlers.js";
|
||||
import routes from "./routes/routes.js";
|
||||
import config from "./services/config.js";
|
||||
import { startScheduledCleanup } from "./services/erase.js";
|
||||
import log from "./services/log.js";
|
||||
import openID from "./services/open_id.js";
|
||||
import { RESOURCE_DIR } from "./services/resource_dir.js";
|
||||
import sql_init from "./services/sql_init.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
|
||||
export default async function buildApp() {
|
||||
const app = express();
|
||||
@@ -33,7 +35,7 @@ export default async function buildApp() {
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(assetsDir, "views"));
|
||||
app.engine("ejs", (await import("ejs")).renderFile);
|
||||
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
app.use((req, res, next) => {
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 230 B |
|
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 265 B |
@@ -1,36 +1,43 @@
|
||||
<p>The SQL Console is Trilium's built-in database editor.</p>
|
||||
<p>It can be accessed by going to the <a href="#root/_help_Vc8PjrjAGuOp">global menu</a> →
|
||||
<p>It can be accessed by going to the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a> →
|
||||
Advanced → Open SQL Console.</p>
|
||||
<p>
|
||||
<img src="SQL Console_image.png">
|
||||
</p>
|
||||
<h3>Interaction</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Hovering the mouse over one of the tables listed at the top of the document
|
||||
will show the columns and their data type.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Only one SQL statement can be run at once.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To run the statement, press the
|
||||
<img src="3_SQL Console_image.png">icon.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For queries that return a result, the data will displayed in a table.</p>
|
||||
<p>
|
||||
<img src="1_SQL Console_image.png">
|
||||
</p>
|
||||
</li>
|
||||
<li>Hovering the mouse over one of the tables listed at the top of the document
|
||||
will show the columns and their data type.</li>
|
||||
<li>Only one SQL statement can be run at once.</li>
|
||||
<li>To run the statement, press the <em>Execute</em> icon.</li>
|
||||
<li>For queries that return a result, the data will displayed in a table.</li>
|
||||
<li>For statements (e.g. <code spellcheck="false">INSERT</code>, <code spellcheck="false">UPDATE</code>),
|
||||
the number of affected rows is displayed.</li>
|
||||
</ul>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:1124/571;" src="2_SQL Console_image.png"
|
||||
width="1124" height="571">
|
||||
</figure>
|
||||
|
||||
<h3>Interacting with the table</h3>
|
||||
<p>After executing a query, a table with the results will be displayed:</p>
|
||||
<ul>
|
||||
<li>Clicking on a column allows sorting ascending or descending.</li>
|
||||
<li>Underneath each column there is an input field which allows filtering
|
||||
by text.</li>
|
||||
<li>Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy the current cell to clipboard.</li>
|
||||
<li>Multiple cells can be selected by dragging or by holding <kbd>Shift</kbd> +
|
||||
arrow keys</li>
|
||||
<li>Results are paginated for performance reasons. The controls at the bottom
|
||||
of the table can be used to navigate through pages.</li>
|
||||
</ul>
|
||||
<h3>Saved SQL console</h3>
|
||||
<p>SQL queries or commands can be saved into a dedicated note.</p>
|
||||
<p>To do so, simply write the query and press the
|
||||
<img src="2_SQL Console_image.png">button. Once saved, the note will appear in <a href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
|
||||
<ul>
|
||||
<li>The SQL expression will not be displayed by default, but it can still
|
||||
be viewed by going to the note context menu and selecting <em>Note source</em>.</li>
|
||||
<li>The expression cannot be modified. If needed, recreate it by copying the
|
||||
statement back into the SQL console and then saving it again.</li>
|
||||
</ul>
|
||||
<img src="1_SQL Console_image.png">button. Once saved, the note will appear in <a class="reference-link"
|
||||
href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
|
||||
<p>The note can be locked for editing by pressing the <em>Lock</em> button
|
||||
in the note actions section near the title bar (on the <a class="reference-link"
|
||||
href="#root/_help_IjZS7iK5EXtb">New Layout</a>, or in the <a class="reference-link"
|
||||
href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> area if using the old
|
||||
layout). When editing is locked, the SQL statement is hidden from view.</p>
|
||||
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
generated
vendored
@@ -38,17 +38,17 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e26b4ce9ba4e9dfe224d04e0f341925ed">Table of contents.</li>
|
||||
<li data-list-item-id="e9707fdfa2c92d66690cf932f7e647253">Syntax highlight of code blocks, provided a language is selected (does
|
||||
<li>Table of contents.</li>
|
||||
<li>Syntax highlight of code blocks, provided a language is selected (does
|
||||
not work if “Auto-detected” is enabled).</li>
|
||||
<li data-list-item-id="e84420a10c6d64bd107edb6e867c91d4b">Rendering for math equations.</li>
|
||||
<li data-list-item-id="e10834dcd0619d77ae2e94d3695bedf58"><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
|
||||
<li>Rendering for math equations.</li>
|
||||
<li><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
|
||||
notes are also shared).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e41cc4139377f9f88d653d1eb8ca47bb4">Inline Mermaid diagrams are not rendered.</li>
|
||||
<li>Inline Mermaid diagrams are not rendered.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -57,12 +57,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e291ae6d5130677b4c99f7c3bdbe974b4">Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
<li>Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e0270680bbdd7a129306e61e11691e36d">No syntax highlight.</li>
|
||||
<li>No syntax highlight.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -95,12 +95,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ea031e1d4149eb443ace756234490c5a4">The child notes are displayed in a fixed format. </li>
|
||||
<li>The child notes are displayed in a fixed format. </li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ea4a9d424aec2afbaecc07bbf64b7bebd">More advanced view types such as the calendar view are not supported.</li>
|
||||
<li>More advanced view types such as the calendar view are not supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,12 +109,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e582d283f2b1b30cbe5ae35d8e01b2bf2">The diagram is displayed as a vector image.</li>
|
||||
<li>The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e33268686446e3c217077201bb5964364">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -123,12 +123,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e443dd0e97c30cb12c77e8906a71569ea">The diagram is displayed as a vector image.</li>
|
||||
<li>The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="efe151ef3f3826c825416417525fb5fb2">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -144,7 +144,7 @@ class="image">
|
||||
<td>The diagram is displayed as a vector image.</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ed3b4fb473042f6e32b4502d4fa11a767">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -160,7 +160,7 @@ class="image">
|
||||
<td>Basic interaction (downloading the file).</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ed87e836a39d127ebcbb33e9e59045afb">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -392,8 +392,8 @@ for (const attr of parentNote.attributes) {
|
||||
<p>Indicates to web crawlers that the page should not be indexed of this
|
||||
note by:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
|
||||
<li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li>
|
||||
<li>Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
|
||||
<li>Setting the <code>noindex, follow</code> meta tag.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,16 +1,79 @@
|
||||
<p>Trilium can import ENEX files which are used by Evernote for backup/export.
|
||||
One ENEX file represents content (notes and resources) of one notebook.</p>
|
||||
<p>Trilium can import ENEX files, which are used by Evernote for backup/export.
|
||||
One ENEX file represents the content (notes and resources) of one notebook.</p>
|
||||
<h2>Export ENEX from Evernote</h2>
|
||||
<p>To export ENEX file, you need to have a <em>legacy</em> desktop version
|
||||
of Evernote (i.e. not web/mobile). Right click on notebook and select export
|
||||
and follow the wizard.</p>
|
||||
<p>To export ENEX files from Evernote, you can use:</p>
|
||||
<ul>
|
||||
<li>Evernote desktop application. See Evernote <a href="https://help.evernote.com/hc/en-us/articles/209005557-Export-Notes-and-Notebooks-as-ENEX-or-HTML">documentation</a>.
|
||||
Note that the limitation of this method is that you can only export 100
|
||||
notes at a time or one notebook at a time.</li>
|
||||
<li>A third-party <a href="https://github.com/vzhd1701/evernote-backup">evernote-backup</a> CLI
|
||||
tool. This tool can export all of your notebooks in bulk.</li>
|
||||
</ul>
|
||||
<h2>Import ENEX in Trilium</h2>
|
||||
<p>Once you have ENEX file, you can import it to Trilium. Right click on
|
||||
some note (to which you want to import the file), click on "Import" and
|
||||
select the ENEX file.</p>
|
||||
<p>After importing the ENEX file, go over the imported notes and resources
|
||||
to be sure the import went well, and you didn't lose any data.</p>
|
||||
<p>Once you have your ENEX files, do the following to import them in Trilium:</p>
|
||||
<ol>
|
||||
<li>In the Trilium note tree, right-click the note under which you want to
|
||||
import one or more of your ENEX files. The notes in the files will be imported
|
||||
as child notes of the selected note.</li>
|
||||
<li>Click Import into note.</li>
|
||||
<li>Choose your ENEX file or files and click Import.</li>
|
||||
<li>During the import, you will see "Import in progress" message. If the import
|
||||
is successful, the message will change to “Import finished successfully”
|
||||
and then disappear.</li>
|
||||
<li>We recommend you to check the imported notes and their attachments to
|
||||
verify that you haven’t lost any data.</li>
|
||||
</ol>
|
||||
<p>A non-exhaustive list of what the importer preserves:</p>
|
||||
<ul>
|
||||
<li>Attachments</li>
|
||||
<li>The hierarchy of headings (these are shifted to start with H2 because
|
||||
H1 is reserved for note title, see <a href="#root/_help_Gr6xFaF6ioJ5">Headings</a>)</li>
|
||||
<li>Tables</li>
|
||||
<li>Bulleted lists</li>
|
||||
<li>Numbered lists</li>
|
||||
<li>Bold</li>
|
||||
<li>Italics</li>
|
||||
<li>Strikethrough</li>
|
||||
<li>Highlights</li>
|
||||
<li>Font colors</li>
|
||||
<li>Soft line breaks</li>
|
||||
<li>External links</li>
|
||||
</ul>
|
||||
<p>However, we do not guarantee that all of your formatting will be imported
|
||||
100% correctly.</p>
|
||||
<h2>Limitations</h2>
|
||||
<p>All resources (except for images) are created as note's attachments.</p>
|
||||
<p>HTML inside ENEX files is not exactly valid so some formatting maybe broken
|
||||
or lost. You can report major problems into <a href="https://github.com/TriliumNext/Trilium/issues">Trilium issue tracker</a>.</p>
|
||||
<ul>
|
||||
<li>The size limit of one import is 250Mb. If the total size of your files
|
||||
is larger, you can increase the <a href="#root/_help_WOcw2SLH6tbX">upload limit</a>,
|
||||
or divide your files, and run the import as many times as necessary.</li>
|
||||
<li>All resources (except for images) are created as notes’ attachments.</li>
|
||||
<li>If you have HTML inside ENEX files, the HTML formatting may be broken
|
||||
or lost after import in Trilium. See <a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
|
||||
</ul>
|
||||
<h3>Internal links</h3>
|
||||
<p>The importer cannot transform Evernote internal links into Trilium internal
|
||||
links because Evernote internal note IDs are not preserved in ENEX files.</p>
|
||||
<p>If you want to restore the internal links in Trilium after you import
|
||||
all of your ENEX files, you can use or adapt this custom script:
|
||||
<a
|
||||
class="reference-link" href="#root/_help_dj3j8dG4th4l">Process internal links by title</a>
|
||||
</p>
|
||||
<p>The script does the following:</p>
|
||||
<ol>
|
||||
<li>It finds all Evernote internal links.</li>
|
||||
<li>For each one, it checks if its link text matches a note title, and if
|
||||
yes, it replaces the Evernote link with an internal Trilium link. If not,
|
||||
it leaves the Evernote link in place.</li>
|
||||
<li>If it finds more than one note with a matching note title, it leaves the
|
||||
Evernote link in place.</li>
|
||||
<li>It outputs the results in a log that you can see in the respective code
|
||||
note in Trilium.</li>
|
||||
</ol>
|
||||
<p>The script has the following limitations:</p>
|
||||
<ul>
|
||||
<li>It will not fix links to anchors and links to notes that you renamed in
|
||||
Evernote after you created the links.</li>
|
||||
<li>Some note titles might not be well identified, even if they exist. This
|
||||
is especially the case if the note title contains some special characters.
|
||||
Should this be problematic, consider <a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,35 @@
|
||||
const query = `note.type = "text" and note.content *=* "evernote:///view/"`;
|
||||
const notes = api.searchForNotes(query);
|
||||
|
||||
for (const note of notes) {
|
||||
api.log(`Processing note ${note.title}...`);
|
||||
|
||||
const content = note.getContent();
|
||||
const $ = api.cheerio.load(content);
|
||||
|
||||
$("a").each((i, el) => {
|
||||
const $el = $(el);
|
||||
|
||||
const url = $el.attr("href");
|
||||
if (!url.startsWith("evernote:///")) return;
|
||||
|
||||
const text = $el.text();
|
||||
const matchingNotes = api.searchForNotes(`note.title = "${text}"`);
|
||||
if (matchingNotes.length === 0) {
|
||||
api.log(`No matching notes for "${text}..."`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchingNotes.length > 1) {
|
||||
api.log(`Found multiple matching notes for "${text}". Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingNote = matchingNotes[0];
|
||||
|
||||
api.log(`Found matching note: ${matchingNote.title} ${matchingNote.noteId}`);
|
||||
$el.attr("href", `#root/${matchingNote.noteId}`);
|
||||
$el.addClass("reference-link");
|
||||
});
|
||||
note.setContent($("body").html());
|
||||
}
|
||||
@@ -8,39 +8,37 @@
|
||||
the number of items stays small. When a note has a large number of notes
|
||||
(in the order of thousands or tens of thousands), two problems arise:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
<li>Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
with a large amount of notes.</li>
|
||||
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">The large amount of notes can slow down the application considerably.</li>
|
||||
<li>The large amount of notes can slow down the application considerably.</li>
|
||||
</ul>
|
||||
<p>Since v0.102.0, Trilium allows the tree to hide the child notes of particular
|
||||
notes. This works for both <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
notes. This works for both <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
normal notes.</p>
|
||||
<h2>Interaction</h2>
|
||||
<p>When the subtree of a note is hidden, there are a few subtle changes:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">To indicate that the subtree is hidden, the note will not have an expand
|
||||
<li>To indicate that the subtree is hidden, the note will not have an expand
|
||||
button and it will display the number of children to the right.</li>
|
||||
<li
|
||||
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
|
||||
<li>It's not possible to add a new note directly from the tree.
|
||||
<ul>
|
||||
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
<li>For <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
it's best to use the built-in mechanism to create notes (for example by
|
||||
creating a new point on a geo-map, or by adding a new row in a table).</li>
|
||||
<li
|
||||
data-list-item-id="e7db44100046c8c79bf79841285aacd1f">For normal notes, it's still possible to create children via other means
|
||||
such as using the <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a> system.</li>
|
||||
<li>For normal notes, it's still possible to create children via other means
|
||||
such as using the <a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a> system.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb049f46cf91db6de113af1099a14944e">Notes can be dragged from outside the note, case in which they will be
|
||||
cloned into it.
|
||||
<ul>
|
||||
<li data-list-item-id="e96d9b7a0755e9c054bab5db4fc1aa25e">Instead of switching to the child notes that were copied, the parent note
|
||||
is highlighted instead.</li>
|
||||
<li data-list-item-id="ec667e3f94a0cfa3fa41ce38d3ed6ee95">A notification will indicate this behavior.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb64670dd7ace6764c18602b440f88049">Similarly, features such as cut/copy and then paste into the note will
|
||||
also work.</li>
|
||||
</li>
|
||||
<li>Notes can be dragged from outside the note, case in which they will be
|
||||
cloned into it.
|
||||
<ul>
|
||||
<li>Instead of switching to the child notes that were copied, the parent note
|
||||
is highlighted instead.</li>
|
||||
<li>A notification will indicate this behavior.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Similarly, features such as cut/copy and then paste into the note will
|
||||
also work.</li>
|
||||
</ul>
|
||||
<h2>Spotlighting</h2>
|
||||
<figure class="image image-style-align-right">
|
||||
@@ -52,12 +50,11 @@
|
||||
<p>During this state, the note remains under its normal hierarchy, so that
|
||||
its easy to tell its location. In addition, this means that:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e2490369eb3d99ca694dba23a3410abef">The note position is clearly visible when using the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</li>
|
||||
<li
|
||||
data-list-item-id="e041d3807f80dc77b022540b0551b8376">The note can still be operated on from the tree, such as adding a
|
||||
<li>The note position is clearly visible when using the <a class="reference-link"
|
||||
href="#root/_help_eIg8jdvaoNNd">Search</a>.</li>
|
||||
<li>The note can still be operated on from the tree, such as adding a
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
class="reference-link" href="#root/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
</ul>
|
||||
<p>The note appears in italics to indicate its temporary display. When switching
|
||||
to another note, the spotlighted note will disappear.</p>
|
||||
@@ -67,29 +64,27 @@
|
||||
This is intentional to avoid displaying a partial state of the subtree.</p>
|
||||
</aside>
|
||||
<h2>Working with collections</h2>
|
||||
<p>By default, some of the <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
<p>By default, some of the <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
automatically hide their child notes, for example the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a> or
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
href="#root/_help_CtBQqbwXDx1w">Kanban Board</a> or the <a class="reference-link"
|
||||
href="#root/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
<p>The reasoning behind this is that collections are generally opaque to
|
||||
the rest of the notes and they can generate a large amount of sub-notes
|
||||
since they intentionally lack structure (in order to allow easy swapping
|
||||
between views).</p>
|
||||
<p>Some types of collections have the child notes intentionally shown, for
|
||||
example the legacy ones (Grid and List), but also the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation</a> which
|
||||
requires the tree structure in order to organize and edit the slides.</p>
|
||||
href="#root/_help_zP3PMqaG71Ct">Presentation</a> which requires the tree
|
||||
structure in order to organize and edit the slides.</p>
|
||||
<p>To toggle this behavior:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
<li>In the <a class="reference-link" href="#root/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
press the Options button underneath the title and uncheck <em>Hide child notes in tree</em>.</li>
|
||||
<li
|
||||
data-list-item-id="e2398432e127c54239d679a6b13d8390b">Right click the collection note in the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Show subtree</em>.</li>
|
||||
<li>Right click the collection note in the <a class="reference-link"
|
||||
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and select <em>Advanced</em> → <em>Show subtree</em>.</li>
|
||||
</ul>
|
||||
<h2>Working with normal notes</h2>
|
||||
<p>It's possible to hide the subtree for normal notes as well, not just collections.
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Hide subtree.</em>
|
||||
</p>
|
||||
8
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
@@ -148,10 +148,10 @@
|
||||
<td>
|
||||
<p>Which view to display in the calendar:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e2cd230dc41f41fe91ee74d7d1fa87372"><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li data-list-item-id="eee1dba4c6cc51ebd53d0a0dd52044cd6"><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li data-list-item-id="ed8721a76a1865dac882415f662ed45b9"><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li data-list-item-id="edf09a13759102d98dac34c33eb690c05"><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
</ul>
|
||||
<p>Any other value will be dismissed and the default view (month) will be
|
||||
used instead.</p>
|
||||
|
||||
68
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
@@ -33,12 +33,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e04c84d59d44645ee89b2a8541ed99f90">Headings (section titles, paragraph)</li>
|
||||
<li data-list-item-id="e39d25bd3d8bd06185b9d259e5827d451">Font size</li>
|
||||
<li data-list-item-id="e1f7e2a2f4b03449d82bdf5b5c6ea8d44">Bold, italic, underline, strike-through</li>
|
||||
<li data-list-item-id="e3decae72884f65b4d538151b6a297072">Superscript, subscript</li>
|
||||
<li data-list-item-id="e59adf00fef65304c163ae190fac5e92a">Font color & background color</li>
|
||||
<li data-list-item-id="ed3f09156147a2769e91db111c76376e2">Remove formatting</li>
|
||||
<li>Headings (section titles, paragraph)</li>
|
||||
<li>Font size</li>
|
||||
<li>Bold, italic, underline, strike-through</li>
|
||||
<li>Superscript, subscript</li>
|
||||
<li>Font color & background color</li>
|
||||
<li>Remove formatting</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -47,9 +47,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ee87806a913900d85d8f018af81f41df8">Bulleted lists</li>
|
||||
<li data-list-item-id="e3ae314e365fa418ca6e0f061d63834c5">Numbered lists</li>
|
||||
<li data-list-item-id="ee84e08694165f95430046cb34f4cd123">To-do lists</li>
|
||||
<li>Bulleted lists</li>
|
||||
<li>Numbered lists</li>
|
||||
<li>To-do lists</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -58,8 +58,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li>
|
||||
<li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li>
|
||||
<li>Block quotes</li>
|
||||
<li>Admonitions</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -68,10 +68,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eb358a4567d93f66004f4195df2dda05a">Basic tables</li>
|
||||
<li data-list-item-id="e6135a555d6c63c30e4b84806a4870830">Merging cells</li>
|
||||
<li data-list-item-id="e29ac76563d0998b28fb1baf94dbdac8c">Styling tables and cells.</li>
|
||||
<li data-list-item-id="e372446e81fdedada64b8bed89ca93d1a">Table captions</li>
|
||||
<li>Basic tables</li>
|
||||
<li>Merging cells</li>
|
||||
<li>Styling tables and cells.</li>
|
||||
<li>Table captions</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -80,9 +80,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eb260b76afcbc07bd9d4ceec4e000e8a0">Inline code</li>
|
||||
<li data-list-item-id="e9864352286369ebe7b41c1599f498de8">Code blocks</li>
|
||||
<li data-list-item-id="ee62fb9ed7f349178e8f2a2bd9ec8cd74">Keyboard shortcuts</li>
|
||||
<li>Inline code</li>
|
||||
<li>Code blocks</li>
|
||||
<li>Keyboard shortcuts</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -91,7 +91,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li>
|
||||
<li>Footnotes</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -100,7 +100,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li>
|
||||
<li>Images</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,8 +109,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li>
|
||||
<li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li>
|
||||
<li>External links</li>
|
||||
<li>Internal Trilium links</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,7 +119,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li>
|
||||
<li>Include note</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -128,12 +128,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e5cdf5d3885ec0ea67f924b4b8fe5c483">Symbols</li>
|
||||
<li data-list-item-id="e95082e6642ed5b1eec6e4e116b899a40"><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
|
||||
<li>Symbols</li>
|
||||
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
|
||||
</li>
|
||||
<li data-list-item-id="ecbef6a358a5b8d27f0d3e08bbc750aa9">Mermaid diagrams</li>
|
||||
<li data-list-item-id="e6e97ee14dd29b7ccf53227107e5dc72d">Horizontal ruler</li>
|
||||
<li data-list-item-id="e6198c7c535c249faec2e8906775f11de">Page break</li>
|
||||
<li>Mermaid diagrams</li>
|
||||
<li>Horizontal ruler</li>
|
||||
<li>Page break</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -142,12 +142,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation
|
||||
<li>Indentation
|
||||
<ul>
|
||||
<li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li>
|
||||
<li>Markdown import</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -157,11 +157,11 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
</li>
|
||||
<li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
</li>
|
||||
<li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
|
||||
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
<td>
|
||||
<p>Defines on which events script should run. Possible values are:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
but not on mobile.</li>
|
||||
<li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
on mobile.</li>
|
||||
<li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code>backendStartup</code> - when Trilium backend starts up</li>
|
||||
<li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
|
||||
<li><code>backendStartup</code> - when Trilium backend starts up</li>
|
||||
<li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
|
||||
specify at which hour, on the back-end.</li>
|
||||
<li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code>daily</code> - run once a day, on the back-end</li>
|
||||
<li><code>daily</code> - run once a day, on the back-end</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Breaking changes.html
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<h2>v0.102.0: Upgrade to jQuery 4.0.0</h2>
|
||||
<p>jQuery 4 removes legacy browser support (such as IE11 support), but it
|
||||
also removes some APIs that are considered deprecated such as:</p>
|
||||
<blockquote>
|
||||
<p><code spellcheck="false">jQuery.isArray</code>, <code spellcheck="false">jQuery.parseJSON</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.trim</code>, <code spellcheck="false">jQuery.type</code>, <code spellcheck="false">jQuery.now</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.isNumeric</code>, <code spellcheck="false">jQuery.isFunction</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.isWindow</code>, <code spellcheck="false">jQuery.camelCase</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.nodeName</code>, <code spellcheck="false">jQuery.cssNumber</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.cssProps</code>, and <code spellcheck="false">jQuery.fx.interval</code>.</p>
|
||||
<p>Use native equivalents like <code spellcheck="false">Array.isArray()</code>,
|
||||
<code
|
||||
spellcheck="false">JSON.parse()</code>, <code spellcheck="false">String.prototype.trim()</code>,
|
||||
and <code spellcheck="false">Date.now()</code> instead.</p>
|
||||
</blockquote>
|
||||
<p>This may affect custom scripts if they (or the custom jQuery libraries
|
||||
used) depend on the deprecated APIs.</p>
|
||||
<p>Note that Trilium polyfills <code spellcheck="false">jQuery.isArray</code>,
|
||||
<code
|
||||
spellcheck="false">isFunction</code>and <code spellcheck="false">isPlainObject</code> because
|
||||
they were required by one of our dependencies (the autocomplete).</p>
|
||||
<p>For more information, consult <a href="https://blog.jquery.com/2026/01/17/jquery-4-0-0/">the official blog post</a>.</p>
|
||||
@@ -107,10 +107,10 @@ class="ck-table-resized">
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ec06332efcc3039721606c052f0d913fa">The widget must export a <code>class</code> and not an instance of the class
|
||||
<li>The widget must export a <code>class</code> and not an instance of the class
|
||||
(e.g. <code>no new</code>) because it needs to be multiplied for each note,
|
||||
so that splits work correctly.</li>
|
||||
<li data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
|
||||
<li>Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
|
||||
must be <code>static</code>, otherwise the widget is ignored.</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -124,7 +124,7 @@ class="ck-table-resized">
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="efe008d361e224f422582552648e1afe7">Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
|
||||
<li>Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
|
||||
of a <code>BasicWidget</code> or a <code>NoteContextAwareWidget</code>.</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
@@ -381,8 +381,6 @@
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Quit Trilium",
|
||||
"recents": "Recent notes",
|
||||
"recently-closed-windows": "Recently closed windows",
|
||||
"tabs-total": "{{number}} tabs total",
|
||||
"bookmarks": "Bookmarks",
|
||||
"today": "Open today's journal note",
|
||||
"new-note": "New note",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
|
||||
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
|
||||
"delete-note": "नोट डिलीट करें",
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें"
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें",
|
||||
"move-note-down-in-hierarchy": "नोट एक लेवल नीचे ले जाएँ",
|
||||
"dialogs": "डायलॉग्स"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
|
||||
"creating-and-moving-notes": "Lage og flytte notater",
|
||||
"dialogs": "Dialogbokser",
|
||||
"other": "Andre"
|
||||
"other": "Andre",
|
||||
"expand-subtree": "Utvid undertre for gjeldende notat"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"step6-here": "her"
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"move-note-up": "Notu bir üste taşı",
|
||||
"collapse-tree": "Tüm not ağacını daraltır",
|
||||
"collapse-subtree": "Geçerli notun alt ağacını daraltır",
|
||||
"sort-child-notes": "Alt notları sırala"
|
||||
"sort-child-notes": "Alt notları sırala",
|
||||
"creating-and-moving-notes": "Notları oluşturma ve yerlerini değiştirme",
|
||||
"create-note-into": "Aktif nota bağlı alt not oluştur",
|
||||
"create-note-after": "Aktif nottan sonra yeni bir not oluştur",
|
||||
"delete-note": "Notu sil",
|
||||
"move-note-down": "Notu aşağıya kaydır"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import { dayjs, getNoteIcon } from "@triliumnext/commons";
|
||||
|
||||
import cloningService from "../../services/cloning.js";
|
||||
import dateUtils from "../../services/date_utils.js";
|
||||
@@ -24,26 +24,6 @@ import BRevision from "./brevision.js";
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
export const NOTE_TYPE_ICONS = {
|
||||
file: "bx bx-file",
|
||||
image: "bx bx-image",
|
||||
code: "bx bx-code",
|
||||
render: "bx bx-extension",
|
||||
search: "bx bx-file-find",
|
||||
relationMap: "bx bxs-network-chart",
|
||||
book: "bx bx-book",
|
||||
noteMap: "bx bxs-network-chart",
|
||||
mermaid: "bx bx-selection",
|
||||
canvas: "bx bx-pen",
|
||||
webView: "bx bx-globe-alt",
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt"
|
||||
};
|
||||
|
||||
interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
isInHoistedSubTree: boolean;
|
||||
@@ -1698,30 +1678,17 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
workspaceIconClass: undefined,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
} else if (this.noteId === "root") {
|
||||
return "bx bx-home-alt-2";
|
||||
}
|
||||
if (this.noteId === "_share") {
|
||||
return "bx bx-share-alt";
|
||||
} else if (this.type === "text") {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
return "bx bx-note";
|
||||
|
||||
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
||||
return "bx bx-data";
|
||||
}
|
||||
return NOTE_TYPE_ICONS[this.type];
|
||||
return `tn-icon ${icon}`;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
|
||||
export default () => {
|
||||
cls.init(() => {
|
||||
const row = sql.getRow<{ value: string }>(
|
||||
`SELECT value FROM options WHERE name = 'openNoteContexts'`
|
||||
);
|
||||
|
||||
if (!row || !row.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(row.value);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already in new format (array + windowId), skip
|
||||
if (
|
||||
Array.isArray(parsed) &&
|
||||
parsed.length > 0 &&
|
||||
parsed[0] &&
|
||||
typeof parsed[0] === "object" &&
|
||||
parsed[0].windowId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Old format: just contexts
|
||||
const migrated = [
|
||||
{
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: parsed
|
||||
}
|
||||
];
|
||||
|
||||
sql.execute(
|
||||
`UPDATE options SET value = ? WHERE name = 'openNoteContexts'`,
|
||||
[JSON.stringify(migrated)]
|
||||
);
|
||||
|
||||
});
|
||||
};
|
||||
@@ -6,11 +6,6 @@
|
||||
|
||||
// Migrations should be kept in descending order, so the latest migration is first.
|
||||
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
|
||||
// Migrate openNoteContexts option to the new structured format with window metadata
|
||||
{
|
||||
version: 234,
|
||||
module: async () => import("./0234__migrate_open_note_contexts_format")
|
||||
},
|
||||
// Migrate geo map to collection
|
||||
{
|
||||
version: 233,
|
||||
|
||||
@@ -50,7 +50,6 @@ export function bootstrap(req: Request, res: Response) {
|
||||
appCssNoteIds: getAppCssNoteIds(),
|
||||
isDev,
|
||||
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
|
||||
windowId: req.query.extraWindow ?? "main",
|
||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||
triliumVersion: packageJson.version,
|
||||
assetPath,
|
||||
|
||||