Compare commits
39 Commits
renovate/n
...
feat/extra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91237918d8 | ||
|
|
3f207663aa | ||
|
|
21cb896849 | ||
|
|
b9bcb07b6d | ||
|
|
787b180378 | ||
|
|
80404b83b0 | ||
|
|
c612bdbfc1 | ||
|
|
3a9e686533 | ||
|
|
9e8d89a170 | ||
|
|
31c70938d6 | ||
|
|
07f3c48d0b | ||
|
|
2821b6da9d | ||
|
|
daba7c398d | ||
|
|
de1ef5b98b | ||
|
|
1bb206d978 | ||
|
|
2fd5ddab86 | ||
|
|
27dc662636 | ||
|
|
52691b5c8c | ||
|
|
8087ed5688 | ||
|
|
79e2c97882 | ||
|
|
1078107776 | ||
|
|
9c9e123e3d | ||
|
|
a8c2947062 | ||
|
|
366166a561 | ||
|
|
4d2b02eddb | ||
|
|
07871853a5 | ||
|
|
254145f0e5 | ||
|
|
c28f11336e | ||
|
|
2e30683b7b | ||
|
|
0af7b8b145 | ||
|
|
5d39b84886 | ||
|
|
537c4051cc | ||
|
|
d0a22bc517 | ||
|
|
19a75acf3f | ||
|
|
3f0abce874 | ||
|
|
36dd29f919 | ||
|
|
d7838f0b67 | ||
|
|
3353d4f436 | ||
|
|
7740154bdc |
@@ -9,7 +9,7 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.5",
|
||||
"archiver": "7.0.1",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery": "3.7.1",
|
||||
"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.6.1",
|
||||
"mind-elixir": "5.5.0",
|
||||
"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.3.3",
|
||||
"happy-dom": "20.1.0",
|
||||
"lightningcss": "1.30.2",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.5"
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import { SqlExecuteResults } 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 & {
|
||||
response: SqlExecuteResponse;
|
||||
results: SqlExecuteResults;
|
||||
};
|
||||
readOnlyTemporarilyDisabled: {
|
||||
noteContext: NoteContext;
|
||||
@@ -542,6 +542,7 @@ 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;
|
||||
@@ -550,10 +551,11 @@ export class AppContext extends Component {
|
||||
|
||||
lastSearchString?: string;
|
||||
|
||||
constructor(isMainWindow: boolean) {
|
||||
constructor(isMainWindow: boolean, windowId: string) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
this.windowId = windowId;
|
||||
// non-widget/layout components needed for the application
|
||||
this.components = [];
|
||||
this.beforeUnloadListeners = [];
|
||||
@@ -683,8 +685,7 @@ export class AppContext extends Component {
|
||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on("beforeunload", () => {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
import bundleService from "../services/bundle.js";
|
||||
import utils from "../services/utils.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() {
|
||||
@@ -143,14 +142,15 @@ 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", { extraWindowHash });
|
||||
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||
} else {
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
|
||||
|
||||
window.open(url, "", "width=1000,height=800");
|
||||
}
|
||||
@@ -188,8 +188,13 @@ 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 response = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
|
||||
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 });
|
||||
}
|
||||
|
||||
toastService.showMessage(t("entrypoints.note-executed"));
|
||||
|
||||
@@ -11,6 +11,8 @@ 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;
|
||||
@@ -25,6 +27,13 @@ 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;
|
||||
@@ -41,9 +50,6 @@ export default class TabManager extends Component {
|
||||
this.recentlyClosedTabs = [];
|
||||
|
||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||
if (!appContext.isMainWindow) {
|
||||
return;
|
||||
}
|
||||
if (options.is("databaseReadonly")) {
|
||||
return;
|
||||
}
|
||||
@@ -52,9 +58,21 @@ export default class TabManager extends Component {
|
||||
.map((nc) => nc.getPojoState())
|
||||
.filter((t) => !!t);
|
||||
|
||||
await server.put("options", {
|
||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
||||
});
|
||||
// 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));
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -69,8 +87,13 @@ 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 = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
||||
const noteContextsToOpen = openNoteContexts || [];
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
@@ -119,6 +142,51 @@ 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 { getNoteIcon } from "@triliumnext/commons";
|
||||
import { MIME_TYPES_DICT } from "@triliumnext/commons";
|
||||
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
@@ -13,6 +13,25 @@ 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
|
||||
@@ -563,18 +582,32 @@ export default class FNote {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
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}`;
|
||||
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];
|
||||
}
|
||||
|
||||
getColorClass() {
|
||||
|
||||
@@ -16,17 +16,6 @@ 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() {
|
||||
@@ -50,25 +39,22 @@ async function loadBootstrapCss() {
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const cssToLoad: string[] = [];
|
||||
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`);
|
||||
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`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
@@ -85,7 +71,7 @@ function loadIcons() {
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
`heading-style-${headingStyle}`,
|
||||
@@ -93,7 +79,8 @@ function setBodyAttributes() {
|
||||
`platform-${platform}`,
|
||||
isElectron && "electron",
|
||||
hasNativeTitleBar && "native-titlebar",
|
||||
hasBackgroundEffects && "background-effects"
|
||||
hasBackgroundEffects && "background-effects",
|
||||
!isMainWindow && 'extra-window'
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const classToSet of classesToSet) {
|
||||
@@ -105,17 +92,10 @@ function setBodyAttributes() {
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
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;
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ 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";
|
||||
@@ -161,9 +163,11 @@ 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,9 +29,7 @@ async function main() {
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
const bodyWrapper = document.createElement("div");
|
||||
render(<App note={note} noteId={noteId} />, bodyWrapper);
|
||||
document.body.appendChild(bodyWrapper);
|
||||
render(<App note={note} noteId={noteId} />, document.body);
|
||||
}
|
||||
|
||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||
|
||||
@@ -8,17 +8,6 @@ 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();
|
||||
|
||||
@@ -27,10 +27,6 @@ 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,6 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
|
||||
@@ -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,42 +40,10 @@
|
||||
border-bottom: var(--col-header-bottom-border);
|
||||
background: var(--col-header-background-color);
|
||||
color: var(--col-header-text-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabulator .tabulator-col-content {
|
||||
padding: 8px 4px !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -112,6 +80,7 @@
|
||||
|
||||
.tabulator-tableholder {
|
||||
padding-top: 10px;
|
||||
height: unset !important; /* Don't extend on the full height */
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
@@ -130,14 +99,6 @@
|
||||
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 {
|
||||
@@ -159,14 +120,11 @@
|
||||
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);
|
||||
}
|
||||
@@ -216,6 +174,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
.tabulator-popup-container {
|
||||
@@ -230,27 +192,8 @@
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
:root .tabulator .tabulator-footer {
|
||||
background: transparent;
|
||||
color: var(--main-text-color);
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
border-top: unset;
|
||||
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,7 +13,8 @@ function injectGlobals() {
|
||||
uncheckedWindow.$ = $;
|
||||
uncheckedWindow.WebSocket = () => {};
|
||||
uncheckedWindow.glob = {
|
||||
isMainWindow: true
|
||||
isMainWindow: true,
|
||||
windowId: "main"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1815,11 +1815,7 @@
|
||||
"configure_launchbar": "Configure Launchbar"
|
||||
},
|
||||
"sql_result": {
|
||||
"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"
|
||||
"no_rows": "No rows have been returned for this query"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tables"
|
||||
|
||||
1
apps/client/src/types.d.ts
vendored
@@ -36,6 +36,7 @@ interface CustomGlobals {
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -100,8 +101,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
|
||||
@@ -265,13 +265,9 @@ 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);
|
||||
@@ -322,8 +318,6 @@ 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") {
|
||||
@@ -348,8 +342,9 @@ 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)
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||
|| noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
}
|
||||
@@ -363,8 +358,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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./NoteList.css";
|
||||
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import { Component, VNode } from "preact";
|
||||
import { VNode } from "preact";
|
||||
import { lazy, Suspense } from "preact/compat";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -120,9 +120,7 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
|
||||
}
|
||||
|
||||
const ComponentToRender = viewType && props && isEnabled && (
|
||||
props.media === "print"
|
||||
? ViewComponents[viewType].print ?? ViewComponents[viewType].normal
|
||||
: ViewComponents[viewType].normal
|
||||
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
|
||||
.tabulator-tableholder {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-view-container {
|
||||
@@ -72,4 +68,4 @@
|
||||
inset-inline-start: 0;
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
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 { 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";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { isValidElement, RefObject } from "preact";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -45,9 +43,7 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
|
||||
|
||||
tabulator.on("tableBuilt", () => {
|
||||
tabulatorRef.current = tabulator;
|
||||
if (externalTabulatorRef) {
|
||||
externalTabulatorRef.current = tabulator;
|
||||
}
|
||||
externalTabulatorRef.current = tabulator;
|
||||
onReady?.();
|
||||
});
|
||||
|
||||
@@ -66,15 +62,12 @@ 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(() => {
|
||||
if (!columns) return;
|
||||
tabulatorRef.current?.setColumns(columns);
|
||||
}, [ columns ]);
|
||||
useEffect(() => { tabulatorRef.current?.setData(data) }, [ data ]);
|
||||
useEffect(() => { columns && tabulatorRef.current?.setColumns(columns)}, [ data]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className} />
|
||||
|
||||
@@ -7,7 +7,6 @@ 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";
|
||||
@@ -23,12 +22,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, type, viewScope));
|
||||
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ titleHidden, setTitleHidden ] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setShown(shouldShow(note, type, viewScope));
|
||||
setShown(shouldShow(note?.noteId, type, viewScope));
|
||||
}, [ note, type, viewScope ]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -70,10 +69,9 @@ export default function InlineTitle() {
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShow(note: FNote | null | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
if (viewScope?.viewMode !== "default") return false;
|
||||
if (note?.noteId?.startsWith("_options")) return true;
|
||||
if (note?.isTriliumSqlite()) return false;
|
||||
if (noteId?.startsWith("_options")) return true;
|
||||
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) && !note?.isTriliumSqlite() &&
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
|
||||
<div
|
||||
className="note-type-switcher"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./StatusBar.css";
|
||||
|
||||
import { Locale, NOTE_TYPE_ICONS, NoteType } from "@triliumnext/commons";
|
||||
import { Locale, 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 from "../../entities/fnote";
|
||||
import FNote, { NOTE_TYPE_ICONS } from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
|
||||
@@ -1232,9 +1232,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
refreshCtx.noteIdsToUpdate.add(noteId);
|
||||
}
|
||||
|
||||
const hasNotesToUpdateOrReload = refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0;
|
||||
const hasNoteReorderingChange = loadResults.getNoteReorderings().length > 0;
|
||||
if (hasNotesToUpdateOrReload || hasNoteReorderingChange) {
|
||||
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
}
|
||||
|
||||
@@ -1395,7 +1393,6 @@ 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" | "sqlConsole";
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
@@ -140,10 +140,5 @@ 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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +0,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);
|
||||
height: 100%;
|
||||
|
||||
.tn-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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,8 +184,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <ActionButton
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
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"
|
||||
&& !note?.isTriliumSqlite();
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default";
|
||||
|
||||
const refreshHeight = () => {
|
||||
if (!ref.current) return;
|
||||
@@ -40,6 +37,6 @@ export default function ScrollPadding() {
|
||||
style={{ height }}
|
||||
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
|
||||
/>
|
||||
: <div />
|
||||
);
|
||||
: <div></div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,4 +40,22 @@ 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 { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useState, useEffect, useRef, useCallback } 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 NoItems from "../react/NoItems";
|
||||
import Icon from "../react/Icon";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import PdfAttachments from "./pdf/PdfAttachments";
|
||||
@@ -47,15 +47,14 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
items.length > 0 ? (
|
||||
items
|
||||
) : (
|
||||
<NoItems
|
||||
icon="bx bx-sidebar"
|
||||
text={t("right_pane.empty_message")}
|
||||
>
|
||||
<div className="no-items">
|
||||
<Icon icon="bx bx-sidebar" />
|
||||
{t("right_pane.empty_message")}
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
</NoItems>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
7
apps/client/src/widgets/sql_result.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.sql-result-widget {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-console-result-container td {
|
||||
white-space: preserve;
|
||||
}
|
||||
63
apps/client/src/widgets/sql_result.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
43
apps/client/src/widgets/sql_table_schemas.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.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);
|
||||
}
|
||||
46
apps/client/src/widgets/sql_table_schemas.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
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,81 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import "./SplitEditor.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
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 { 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 ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
className?: string;
|
||||
@@ -17,8 +15,6 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
splitOptions?: Split.Options;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
editorBefore?: ComponentChildren;
|
||||
forceOrientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,14 +26,13 @@ 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, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
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);
|
||||
|
||||
const editor = (!readOnly &&
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
<EditableCode
|
||||
note={note}
|
||||
@@ -79,12 +74,12 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
}, [ readOnly, 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} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
@@ -93,12 +88,11 @@ export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
className="tn-tool-button"
|
||||
noIconActionClass
|
||||
titlePosition="top"
|
||||
/>;
|
||||
/>
|
||||
}
|
||||
|
||||
function useSplitOrientation(forceOrientation?: "horizontal" | "vertical") {
|
||||
function useSplitOrientation() {
|
||||
const [ splitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
if (forceOrientation) return forceOrientation;
|
||||
if (isMobile()) return "vertical";
|
||||
if (!splitEditorOrientation) return "horizontal";
|
||||
return splitEditorOrientation as "horizontal" | "vertical";
|
||||
|
||||
@@ -93,15 +93,7 @@ export default defineConfig(() => ({
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
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";
|
||||
},
|
||||
entryFileNames: "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.2",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"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": "40.0.0",
|
||||
"electron": "39.2.7",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
|
||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 931 B |
BIN
apps/desktop/src/assets/images/tray/closed-windowsTemplate.png
Normal file
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 492 B |
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -69,10 +70,12 @@ async function main() {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("second-instance", (event, commandLine) => {
|
||||
app.on("second-instance", async (event, commandLine) => {
|
||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||
if (commandLine.includes("--new-window")) {
|
||||
windowService.createExtraWindow("");
|
||||
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
|
||||
const extraWindowId = randomString(4);
|
||||
windowService.createExtraWindow(extraWindowId, "");
|
||||
} else if (lastFocusedWindow) {
|
||||
if (lastFocusedWindow.isMinimized()) {
|
||||
lastFocusedWindow.restore();
|
||||
@@ -124,7 +127,8 @@ async function onReady() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await normalizeOpenNoteContexts();
|
||||
tray.createTray();
|
||||
} else {
|
||||
await windowService.createSetupWindow();
|
||||
@@ -133,6 +137,30 @@ 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.2",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"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.2"
|
||||
"better-sqlite3": "12.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@triliumnext/client": "workspace:*",
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron": "39.2.7",
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -185,9 +185,6 @@ 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("10 highlights");
|
||||
await expect(app.sidebar).toContainText(/highlights/i);
|
||||
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.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
FROM node:24.12.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.13.0-bullseye-slim
|
||||
FROM node:24.12.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.13.0-alpine AS builder
|
||||
FROM node:24.12.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.13.0-alpine
|
||||
FROM node:24.12.0-alpine
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
FROM node:24.12.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.13.0-alpine
|
||||
FROM node:24.12.0-alpine
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
FROM node:24.12.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.13.0-bullseye-slim
|
||||
FROM node:24.12.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.2"
|
||||
"better-sqlite3": "12.6.0"
|
||||
}
|
||||
}
|
||||
@@ -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.2",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"node-html-parser": "7.0.2",
|
||||
"sucrase": "3.35.1"
|
||||
@@ -82,8 +82,8 @@
|
||||
"csrf-csrf": "3.2.2",
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "4.0.1",
|
||||
"electron": "40.0.0",
|
||||
"ejs": "3.1.10",
|
||||
"electron": "39.2.7",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -126,7 +126,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turnish": "1.8.0",
|
||||
"turndown": "7.2.2",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
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 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 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";
|
||||
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 { RESOURCE_DIR } from "./services/resource_dir.js";
|
||||
|
||||
export default async function buildApp() {
|
||||
const app = express();
|
||||
@@ -35,7 +33,7 @@ export default async function buildApp() {
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(assetsDir, "views"));
|
||||
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
|
||||
app.engine("ejs", (await import("ejs")).renderFile);
|
||||
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: 230 B After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 230 B |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Database/Manually altering the database/3_SQL Console_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 265 B |
@@ -1,43 +1,36 @@
|
||||
<p>The SQL Console is Trilium's built-in database editor.</p>
|
||||
<p>It can be accessed by going to the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a> →
|
||||
<p>It can be accessed by going to the <a href="#root/_help_Vc8PjrjAGuOp">global menu</a> →
|
||||
Advanced → Open SQL Console.</p>
|
||||
<p>
|
||||
<img src="SQL Console_image.png">
|
||||
</p>
|
||||
<h3>Interaction</h3>
|
||||
<ul>
|
||||
<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>
|
||||
<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>
|
||||
</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="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>
|
||||
<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>
|
||||
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>Table of contents.</li>
|
||||
<li>Syntax highlight of code blocks, provided a language is selected (does
|
||||
<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
|
||||
not work if “Auto-detected” is enabled).</li>
|
||||
<li>Rendering for math equations.</li>
|
||||
<li><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
|
||||
<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
|
||||
notes are also shared).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Inline Mermaid diagrams are not rendered.</li>
|
||||
<li data-list-item-id="e41cc4139377f9f88d653d1eb8ca47bb4">Inline Mermaid diagrams are not rendered.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -57,12 +57,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
<li data-list-item-id="e291ae6d5130677b4c99f7c3bdbe974b4">Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>No syntax highlight.</li>
|
||||
<li data-list-item-id="e0270680bbdd7a129306e61e11691e36d">No syntax highlight.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -95,12 +95,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li>The child notes are displayed in a fixed format. </li>
|
||||
<li data-list-item-id="ea031e1d4149eb443ace756234490c5a4">The child notes are displayed in a fixed format. </li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>More advanced view types such as the calendar view are not supported.</li>
|
||||
<li data-list-item-id="ea4a9d424aec2afbaecc07bbf64b7bebd">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>The diagram is displayed as a vector image.</li>
|
||||
<li data-list-item-id="e582d283f2b1b30cbe5ae35d8e01b2bf2">The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>No further interaction supported.</li>
|
||||
<li data-list-item-id="e33268686446e3c217077201bb5964364">No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -123,12 +123,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li>The diagram is displayed as a vector image.</li>
|
||||
<li data-list-item-id="e443dd0e97c30cb12c77e8906a71569ea">The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>No further interaction supported.</li>
|
||||
<li data-list-item-id="efe151ef3f3826c825416417525fb5fb2">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>No further interaction supported.</li>
|
||||
<li data-list-item-id="ed3b4fb473042f6e32b4502d4fa11a767">No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -160,7 +160,7 @@ class="image">
|
||||
<td>Basic interaction (downloading the file).</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>No further interaction supported.</li>
|
||||
<li data-list-item-id="ed87e836a39d127ebcbb33e9e59045afb">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>Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
|
||||
<li>Setting the <code>noindex, follow</code> meta tag.</li>
|
||||
<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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,79 +1,16 @@
|
||||
<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>
|
||||
<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>
|
||||
<h2>Export ENEX from Evernote</h2>
|
||||
<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>
|
||||
<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>
|
||||
<h2>Import ENEX in Trilium</h2>
|
||||
<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>
|
||||
<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>
|
||||
<h2>Limitations</h2>
|
||||
<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>
|
||||
<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>
|
||||
@@ -1,35 +0,0 @@
|
||||
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,37 +8,39 @@
|
||||
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>Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
with a large amount of notes.</li>
|
||||
<li>The large amount of notes can slow down the application considerably.</li>
|
||||
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">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/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
notes. This works for both <a class="reference-link" href="#root/pOsGYCXsbNQG/_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>To indicate that the subtree is hidden, the note will not have an expand
|
||||
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">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>It's not possible to add a new note directly from the tree.
|
||||
<li
|
||||
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
|
||||
<ul>
|
||||
<li>For <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For <a class="reference-link" href="#root/pOsGYCXsbNQG/_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>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>
|
||||
<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>
|
||||
</ul>
|
||||
</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>
|
||||
</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>
|
||||
</ul>
|
||||
<h2>Spotlighting</h2>
|
||||
<figure class="image image-style-align-right">
|
||||
@@ -50,11 +52,12 @@
|
||||
<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>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
|
||||
<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
|
||||
<a
|
||||
class="reference-link" href="#root/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_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>
|
||||
@@ -64,27 +67,29 @@
|
||||
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/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
<p>By default, some of the <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
automatically hide their child notes, for example the <a class="reference-link"
|
||||
href="#root/_help_CtBQqbwXDx1w">Kanban Board</a> or the <a class="reference-link"
|
||||
href="#root/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a> or
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_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/_help_zP3PMqaG71Ct">Presentation</a> which requires the tree
|
||||
structure in order to organize and edit the slides.</p>
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_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>In the <a class="reference-link" href="#root/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
press the Options button underneath the title and uncheck <em>Hide child notes in tree</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>
|
||||
<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>
|
||||
</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/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_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><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>
|
||||
<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>
|
||||
</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>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>
|
||||
<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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -47,9 +47,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Bulleted lists</li>
|
||||
<li>Numbered lists</li>
|
||||
<li>To-do lists</li>
|
||||
<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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -58,8 +58,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Block quotes</li>
|
||||
<li>Admonitions</li>
|
||||
<li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li>
|
||||
<li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -68,10 +68,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Basic tables</li>
|
||||
<li>Merging cells</li>
|
||||
<li>Styling tables and cells.</li>
|
||||
<li>Table captions</li>
|
||||
<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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -80,9 +80,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Inline code</li>
|
||||
<li>Code blocks</li>
|
||||
<li>Keyboard shortcuts</li>
|
||||
<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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -91,7 +91,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Footnotes</li>
|
||||
<li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -100,7 +100,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Images</li>
|
||||
<li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,8 +109,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>External links</li>
|
||||
<li>Internal Trilium links</li>
|
||||
<li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li>
|
||||
<li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,7 +119,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Include note</li>
|
||||
<li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -128,12 +128,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Symbols</li>
|
||||
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
|
||||
<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>
|
||||
<li>Mermaid diagrams</li>
|
||||
<li>Horizontal ruler</li>
|
||||
<li>Page break</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>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -142,12 +142,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Indentation
|
||||
<li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation
|
||||
<ul>
|
||||
<li>Markdown import</li>
|
||||
<li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
<li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -157,11 +157,11 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
<li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
</li>
|
||||
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
<li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
</li>
|
||||
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
|
||||
<li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><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><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
but not on mobile.</li>
|
||||
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
on mobile.</li>
|
||||
<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
|
||||
<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
|
||||
specify at which hour, on the back-end.</li>
|
||||
<li><code>daily</code> - run once a day, on the back-end</li>
|
||||
<li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code>daily</code> - run once a day, on the back-end</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<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>The widget must export a <code>class</code> and not an instance of the class
|
||||
<li data-list-item-id="ec06332efcc3039721606c052f0d913fa">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>Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
|
||||
<li data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">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>Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
|
||||
<li data-list-item-id="efe008d361e224f422582552648e1afe7">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,6 +381,8 @@
|
||||
"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,8 +27,6 @@
|
||||
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
|
||||
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
|
||||
"delete-note": "नोट डिलीट करें",
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें",
|
||||
"move-note-down-in-hierarchy": "नोट एक लेवल नीचे ले जाएँ",
|
||||
"dialogs": "डायलॉग्स"
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
|
||||
"creating-and-moving-notes": "Lage og flytte notater",
|
||||
"dialogs": "Dialogbokser",
|
||||
"other": "Andre",
|
||||
"expand-subtree": "Utvid undertre for gjeldende notat"
|
||||
"other": "Andre"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"step6-here": "her"
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
"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",
|
||||
"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"
|
||||
"sort-child-notes": "Alt notları sırala"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
|
||||
import { dayjs, getNoteIcon } from "@triliumnext/commons";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
|
||||
import cloningService from "../../services/cloning.js";
|
||||
import dateUtils from "../../services/date_utils.js";
|
||||
@@ -24,6 +24,26 @@ 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;
|
||||
@@ -1678,17 +1698,30 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
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)
|
||||
});
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
return `tn-icon ${icon}`;
|
||||
// TODO: Deduplicate with fnote
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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,6 +6,11 @@
|
||||
|
||||
// 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,6 +50,7 @@ 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,
|
||||
|
||||
@@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
|
||||
import dataDir from "./data_dir.js";
|
||||
import { AppInfo } from "@triliumnext/commons";
|
||||
|
||||
const APP_DB_VERSION = 233;
|
||||
const APP_DB_VERSION = 234;
|
||||
const SYNC_VERSION = 36;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
||||
@@ -387,58 +387,4 @@ describe("Markdown export", () => {
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("maintains escaped HTML tags", () => {
|
||||
const html = /*html*/`<p><div>Hello World</div></p>`;
|
||||
const expected = `\\<div\\>Hello World\\</div\\>`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("escapes HTML tags inside list", () => {
|
||||
const html = trimIndentation/*html*/`\
|
||||
<ul>
|
||||
<li data-list-item-id="e07fda078f7dd7103a3b9017f49eb1589">
|
||||
<note> is note.
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
const expected = trimIndentation`\
|
||||
* \\<note\\> is note.`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("exports jQuery code in table properly", () => {
|
||||
const html = trimIndentation`\
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Code
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<pre>
|
||||
<code class="language-text-x-trilium-auto">this.$widget = $("<div>");</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
`;
|
||||
const expected = trimIndentation`\
|
||||
<table><thead><tr><th>Code</th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">this.$widget = $("<div>");</code>
|
||||
</pre></td></tr></tbody></table>`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("renders emphasis with underscore", () => {
|
||||
const html = /*html*/`<p>This is <em>underlined</em> text.</p>`;
|
||||
const expected = `This is _underlined_ text.`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
import Turnish, { type Rule } from "turnish";
|
||||
"use strict";
|
||||
|
||||
let instance: Turnish | null = null;
|
||||
import TurndownService, { type Rule } from "turndown";
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
|
||||
let instance: TurndownService | null = null;
|
||||
|
||||
// TODO: Move this to a dedicated file someday.
|
||||
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
@@ -14,12 +16,12 @@ export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
|
||||
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
|
||||
|
||||
const fencedCodeBlockFilter: Rule = {
|
||||
filter (node, options) {
|
||||
const fencedCodeBlockFilter: TurndownService.Rule = {
|
||||
filter: function (node, options) {
|
||||
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
|
||||
},
|
||||
|
||||
replacement (content, node, options) {
|
||||
replacement: function (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
}
|
||||
@@ -27,25 +29,23 @@ const fencedCodeBlockFilter: Rule = {
|
||||
const className = node.firstChild.getAttribute("class") || "";
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
|
||||
|
||||
return `\n\n${options.fence}${language}\n${node.firstChild.textContent}\n${options.fence}\n\n`;
|
||||
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
|
||||
}
|
||||
};
|
||||
|
||||
function toMarkdown(content: string) {
|
||||
if (instance === null) {
|
||||
instance = new Turnish({
|
||||
instance = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
bulletListMarker: "*",
|
||||
emDelimiter: "_",
|
||||
codeBlockStyle: "fenced",
|
||||
blankReplacement(_content, node) {
|
||||
if (node.nodeName === "SECTION" && node.classList.contains("include-note")) {
|
||||
return node.outerHTML;
|
||||
blankReplacement(content, node, options) {
|
||||
if (node.nodeName === "SECTION" && (node as HTMLElement).classList.contains("include-note")) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
}
|
||||
|
||||
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
|
||||
},
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : ''
|
||||
}
|
||||
});
|
||||
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
@@ -59,7 +59,7 @@ function toMarkdown(content: string) {
|
||||
instance.keep([ "kbd", "sup", "sub" ]);
|
||||
}
|
||||
|
||||
return instance.render(content);
|
||||
return instance.turndown(content);
|
||||
}
|
||||
|
||||
function rewriteLanguageTag(source: string) {
|
||||
@@ -85,14 +85,14 @@ function buildImageFilter() {
|
||||
const ESCAPE_PATTERNS = {
|
||||
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
|
||||
after: /((?:^\d+(?=\.)))/
|
||||
};
|
||||
}
|
||||
|
||||
const escapePattern = new RegExp(`(?:${ESCAPE_PATTERNS.before.source}|${ESCAPE_PATTERNS.after.source})`, 'g');
|
||||
const escapePattern = new RegExp('(?:' + ESCAPE_PATTERNS.before.source + '|' + ESCAPE_PATTERNS.after.source + ')', 'g');
|
||||
|
||||
function escapeMarkdown (content: string) {
|
||||
return content.replace(escapePattern, (match, before, after) => {
|
||||
return before ? `\\${before}` : `${after}\\`;
|
||||
});
|
||||
return content.replace(escapePattern, function (match, before, after) {
|
||||
return before ? '\\' + before : after + '\\'
|
||||
})
|
||||
}
|
||||
|
||||
function escapeLinkDestination(destination: string) {
|
||||
@@ -102,10 +102,10 @@ function buildImageFilter() {
|
||||
}
|
||||
|
||||
function escapeLinkTitle (title: string) {
|
||||
return title.replace(/"/g, '\\"');
|
||||
return title.replace(/"/g, '\\"')
|
||||
}
|
||||
|
||||
const imageFilter: Rule = {
|
||||
const imageFilter: TurndownService.Rule = {
|
||||
filter: "img",
|
||||
replacement(content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
@@ -117,12 +117,12 @@ function buildImageFilter() {
|
||||
|
||||
// TODO: Deduplicate with upstream.
|
||||
const untypedNode = (node as any);
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'));
|
||||
const titlePart = title ? ` "${escapeLinkTitle(title)}"` : '';
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')))
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '')
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'))
|
||||
const titlePart = title ? ' "' + escapeLinkTitle(title) + '"' : ''
|
||||
|
||||
return src ? `` : '';
|
||||
return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
|
||||
}
|
||||
};
|
||||
return imageFilter;
|
||||
@@ -151,7 +151,7 @@ function buildAdmonitionFilter() {
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const admonitionFilter: Rule = {
|
||||
const admonitionFilter: TurndownService.Rule = {
|
||||
filter(node, options) {
|
||||
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
|
||||
},
|
||||
@@ -161,11 +161,11 @@ function buildAdmonitionFilter() {
|
||||
|
||||
content = content.replace(/^\n+|\n+$/g, '');
|
||||
content = content.replace(/^/gm, '> ');
|
||||
content = `> [!${admonitionType}]\n${content}`;
|
||||
content = `> [!${admonitionType}]\n` + content;
|
||||
|
||||
return `\n\n${content}\n\n`;
|
||||
return "\n\n" + content + "\n\n";
|
||||
}
|
||||
};
|
||||
}
|
||||
return admonitionFilter;
|
||||
}
|
||||
|
||||
@@ -178,15 +178,15 @@ function buildAdmonitionFilter() {
|
||||
*/
|
||||
function buildInlineLinkFilter(): Rule {
|
||||
return {
|
||||
filter (node, options) {
|
||||
filter: function (node, options) {
|
||||
return (
|
||||
options.linkStyle === 'inlined' &&
|
||||
node.nodeName === 'A' &&
|
||||
!!node.getAttribute('href')
|
||||
);
|
||||
)
|
||||
},
|
||||
|
||||
replacement (content, _node) {
|
||||
replacement: function (content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Return reference links verbatim.
|
||||
@@ -196,13 +196,13 @@ function buildInlineLinkFilter(): Rule {
|
||||
|
||||
// Otherwise treat as normal.
|
||||
// TODO: Call super() somehow instead of duplicating the implementation.
|
||||
let href = node.getAttribute('href');
|
||||
if (href) href = href.replace(/([()])/g, '\\$1');
|
||||
let title = cleanAttribute(node.getAttribute('title'));
|
||||
if (title) title = ` "${title.replace(/"/g, '\\"')}"`;
|
||||
return `[${content}](${href}${title})`;
|
||||
let href = node.getAttribute('href')
|
||||
if (href) href = href.replace(/([()])/g, '\\$1')
|
||||
let title = cleanAttribute(node.getAttribute('title'))
|
||||
if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'
|
||||
return '[' + content + '](' + href + title + ')'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildFigureFilter(): Rule {
|
||||
@@ -214,7 +214,7 @@ function buildFigureFilter(): Rule {
|
||||
replacement(content, node) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
@@ -224,13 +224,13 @@ function buildListItemFilter(): Rule {
|
||||
replacement(content, node, options) {
|
||||
content = content
|
||||
.trim()
|
||||
.replace(/\n/gm, '\n '); // indent
|
||||
let prefix = `${options.bulletListMarker} `;
|
||||
.replace(/\n/gm, '\n ') // indent
|
||||
let prefix = options.bulletListMarker + ' '
|
||||
const parent = node.parentNode as HTMLElement;
|
||||
if (parent.nodeName === 'OL') {
|
||||
const start = parent.getAttribute('start');
|
||||
const index = Array.prototype.indexOf.call(parent.children, node);
|
||||
prefix = `${start ? Number(start) + index : index + 1}. `;
|
||||
var start = parent.getAttribute('start')
|
||||
var index = Array.prototype.indexOf.call(parent.children, node)
|
||||
prefix = (start ? Number(start) + index : index + 1) + '. '
|
||||
} else if (parent.classList.contains("todo-list")) {
|
||||
const isChecked = node.querySelector("input[type=checkbox]:checked");
|
||||
prefix = (isChecked ? "- [x] " : "- [ ] ");
|
||||
@@ -239,7 +239,7 @@ function buildListItemFilter(): Rule {
|
||||
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildMathFilter(): Rule {
|
||||
@@ -270,13 +270,13 @@ function buildMathFilter(): Rule {
|
||||
// Unknown.
|
||||
return content;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from upstream since it's not exposed.
|
||||
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
|
||||
function cleanAttribute(attribute: string | null | undefined) {
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -314,9 +314,4 @@ $$`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("preserves HTML entities in list", () => {
|
||||
const input = `* <note> is note.`;
|
||||
const expected = /*html*/`<ul><li><note> is note.</li></ul>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import type { NoteType } from "@triliumnext/commons";
|
||||
"use strict";
|
||||
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import imageService from "../../services/image.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import type { File } from "./common.js";
|
||||
|
||||
import noteService from "../../services/notes.js";
|
||||
import imageService from "../../services/image.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import mimeService from "./mime.js";
|
||||
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import importUtils from "./utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import type { File } from "./common.js";
|
||||
import type { NoteType } from "@triliumnext/commons";
|
||||
|
||||
function importSingleFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
|
||||
const mime = mimeService.getMime(file.originalname) || file.mimetype;
|
||||
@@ -54,14 +56,13 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext<"im
|
||||
function importFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
|
||||
const originalName = file.originalname;
|
||||
|
||||
const mime = mimeService.getMime(originalName) || file.mimetype;
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title: getNoteTitle(originalName, mime === "application/pdf"),
|
||||
title: originalName,
|
||||
content: file.buffer,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "file",
|
||||
mime
|
||||
mime: mimeService.getMime(originalName) || file.mimetype
|
||||
});
|
||||
|
||||
note.addLabel("originalFileName", originalName);
|
||||
@@ -87,7 +88,7 @@ function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, par
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
mime,
|
||||
mime: mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
@@ -105,7 +106,7 @@ function importCustomType(taskContext: TaskContext<"importNotes">, file: File, p
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
mime,
|
||||
mime: mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
@@ -213,7 +214,7 @@ function importAttachment(taskContext: TaskContext<"importNotes">, file: File, p
|
||||
title: file.originalname,
|
||||
content: file.buffer,
|
||||
role: "file",
|
||||
mime
|
||||
mime: mime
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
|
||||
import path from "path";
|
||||
import type { Stream } from "stream";
|
||||
import yauzl from "yauzl";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import BAttachment from "../../becca/entities/battachment.js";
|
||||
import BAttribute from "../../becca/entities/battribute.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer, unescapeHtml } from "../../services/utils.js";
|
||||
import log from "../../services/log.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import { getNoteTitle, newEntityId, processStringOrBuffer, removeFileExtension, unescapeHtml } from "../../services/utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import type AttributeMeta from "../meta/attribute_meta.js";
|
||||
import type NoteMeta from "../meta/note_meta.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import path from "path";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import treeService from "../tree.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import mimeService from "./mime.js";
|
||||
import treeService from "../tree.js";
|
||||
import yauzl from "yauzl";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import becca from "../../becca/becca.js";
|
||||
import BAttachment from "../../becca/entities/battachment.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type NoteMeta from "../meta/note_meta.js";
|
||||
import type AttributeMeta from "../meta/attribute_meta.js";
|
||||
import type { Stream } from "stream";
|
||||
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
|
||||
|
||||
interface MetaFile {
|
||||
files: NoteMeta[];
|
||||
@@ -109,7 +108,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
dataFileName: ""
|
||||
};
|
||||
|
||||
let parent: NoteMeta | undefined;
|
||||
let parent: NoteMeta | undefined = undefined;
|
||||
|
||||
for (let segment of pathSegments) {
|
||||
if (!cursor?.children?.length) {
|
||||
@@ -162,7 +161,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
|
||||
// in case we lack metadata, we treat e.g. "Programming.html" and "Programming" as the same note
|
||||
// (one data file, the other directory for children)
|
||||
const filePathNoExt = removeFileExtension(filePath);
|
||||
const filePathNoExt = removeTextFileExtension(filePath);
|
||||
|
||||
if (filePathNoExt in createdPaths) {
|
||||
return createdPaths[filePathNoExt];
|
||||
@@ -242,10 +241,10 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
}
|
||||
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
parentNoteId: parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content: "",
|
||||
noteId,
|
||||
noteId: noteId,
|
||||
type: resolveNoteType(noteMeta?.type),
|
||||
mime: noteMeta ? noteMeta.mime : "text/html",
|
||||
prefix: noteMeta?.prefix || "",
|
||||
@@ -295,12 +294,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||
noteId: getNewNoteId(noteMeta.noteId)
|
||||
};
|
||||
}
|
||||
// don't check for noteMeta since it's not mandatory for notes
|
||||
return {
|
||||
noteId: getNoteId(noteMeta, absUrl)
|
||||
};
|
||||
|
||||
} else {
|
||||
// don't check for noteMeta since it's not mandatory for notes
|
||||
return {
|
||||
noteId: getNoteId(noteMeta, absUrl)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
|
||||
@@ -313,9 +312,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
|
||||
if (noteTitle.trim() === text.trim()) {
|
||||
return ""; // remove whole H1 tag
|
||||
}
|
||||
return `<h2>${text}</h2>`;
|
||||
|
||||
} else {
|
||||
return `<h2>${text}</h2>`;
|
||||
}
|
||||
});
|
||||
|
||||
if (taskContext.data?.safeImport) {
|
||||
@@ -348,9 +347,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
return `src="api/attachments/${target.attachmentId}/image/${path.basename(url)}"`;
|
||||
} else if (target.noteId) {
|
||||
return `src="api/images/${target.noteId}/${path.basename(url)}"`;
|
||||
}
|
||||
return match;
|
||||
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
|
||||
content = content.replace(/href="([^"]*)"/g, (match, url) => {
|
||||
@@ -374,9 +373,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
return `href="#root/${target.noteId}?viewMode=attachments&attachmentId=${target.attachmentId}"`;
|
||||
} else if (target.noteId) {
|
||||
return `href="#root/${target.noteId}"`;
|
||||
}
|
||||
return match;
|
||||
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
|
||||
if (noteMeta) {
|
||||
@@ -526,9 +525,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
}
|
||||
|
||||
({ note } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
parentNoteId: parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content,
|
||||
content: content,
|
||||
noteId,
|
||||
type,
|
||||
mime,
|
||||
@@ -537,7 +536,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
// root notePosition should be ignored since it relates to the original document
|
||||
// now import root should be placed after existing notes into new parent
|
||||
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
|
||||
isProtected
|
||||
isProtected: isProtected
|
||||
}));
|
||||
|
||||
createdNoteIds.add(note.noteId);
|
||||
@@ -649,7 +648,7 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
|
||||
export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
|
||||
return new Promise((res, rej) => {
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
zipfile.openReadStream(entry, function (err, readStream) {
|
||||
if (err) rej(err);
|
||||
if (!readStream) throw new Error("Unable to read content.");
|
||||
|
||||
@@ -660,7 +659,7 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
|
||||
|
||||
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
|
||||
if (err) rej(err);
|
||||
if (!zipfile) throw new Error("Unable to read zip file.");
|
||||
|
||||
@@ -692,9 +691,9 @@ function resolveNoteType(type: string | undefined): NoteType {
|
||||
|
||||
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
|
||||
return type as NoteType;
|
||||
}
|
||||
return "text";
|
||||
|
||||
} else {
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
|
||||
export function removeTriliumTags(content: string) {
|
||||
@@ -703,7 +702,7 @@ export function removeTriliumTags(content: string) {
|
||||
"<title data-trilium-title>([^<]*)<\/title>"
|
||||
];
|
||||
for (const tag of tagsToRemove) {
|
||||
const re = new RegExp(tag, "gi");
|
||||
let re = new RegExp(tag, "gi");
|
||||
content = content.replace(re, "");
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,19 @@ function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
|
||||
return val === "true";
|
||||
}
|
||||
|
||||
function getOptionJson(name: OptionNames) {
|
||||
const val = getOptionOrNull(name);
|
||||
|
||||
if (typeof val !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
|
||||
const option = becca.getOption(name);
|
||||
|
||||
@@ -137,6 +150,7 @@ export default {
|
||||
getOption,
|
||||
getOptionInt,
|
||||
getOptionBool,
|
||||
getOptionJson,
|
||||
setOption,
|
||||
createOption,
|
||||
getOptions,
|
||||
|
||||
@@ -45,8 +45,15 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
false
|
||||
@@ -257,8 +264,15 @@ function initStartupOptions() {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -147,8 +147,15 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -196,6 +196,39 @@ function updateTrayMenu() {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function buildClosedWindowsMenu() {
|
||||
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
|
||||
const openedWindowIds = windowService.getAllWindowIds();
|
||||
const closedWindows = savedWindows
|
||||
.filter(win => !openedWindowIds.includes(win.windowId))
|
||||
.sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order
|
||||
const menuItems: Electron.MenuItemConstructorOptions[] = [];
|
||||
|
||||
for (let i = closedWindows.length - 1; i >= 0; i--) {
|
||||
const win = closedWindows[i];
|
||||
const activeCtx = win.contexts.find(c => c.active === true);
|
||||
const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath;
|
||||
const activateNoteId = activateNotePath?.split("/").pop() ?? null;
|
||||
if (!activateNoteId) continue;
|
||||
|
||||
// Get the title of the closed window
|
||||
const winTitle = (() => {
|
||||
const raw = becca_service.getNoteTitle(activateNoteId);
|
||||
const truncated = raw.length > 20 ? `${raw.slice(0, 17)}…` : raw;
|
||||
const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length;
|
||||
return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated;
|
||||
})();
|
||||
|
||||
menuItems.push({
|
||||
label: winTitle,
|
||||
type: "normal",
|
||||
click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow()
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Only call getWindowTitle if windowVisibilityMap has more than one window
|
||||
@@ -258,6 +291,12 @@ function updateTrayMenu() {
|
||||
icon: getIconPath("recents"),
|
||||
submenu: buildRecentNotesMenu()
|
||||
},
|
||||
{
|
||||
label: t("tray.recently-closed-windows"),
|
||||
type: "submenu",
|
||||
icon: getIconPath("closed-windows"),
|
||||
submenu: buildClosedWindowsMenu()
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: t("tray.close"),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import {note, NoteBuilder} from "../test/becca_mocking.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { note, NoteBuilder } from "../test/becca_mocking.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import tree from "./tree.js";
|
||||
import cls from "./cls.js";
|
||||
import {buildNote} from "../test/becca_easy_mocking.js";
|
||||
import { buildNote } from "../test/becca_easy_mocking.js";
|
||||
|
||||
describe("Tree", () => {
|
||||
let rootNote!: NoteBuilder;
|
||||
@@ -48,23 +48,6 @@ describe("Tree", () => {
|
||||
};
|
||||
});
|
||||
});
|
||||
it("sorts notes by title (base case)", () => {
|
||||
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "3"},
|
||||
],
|
||||
"#sorted": "",
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["1", "2", "3"]);
|
||||
}
|
||||
)
|
||||
|
||||
it("custom sort order is idempotent", () => {
|
||||
rootNote.label("sorted", "order");
|
||||
@@ -73,15 +56,13 @@ describe("Tree", () => {
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
rootNote.child(note(String(i)).label("order", String(i)));
|
||||
}
|
||||
rootNote.child(note("top").label("top"));
|
||||
rootNote.child(note("bottom").label("bottom"));
|
||||
|
||||
// Add a few values which have no defined order.
|
||||
for (let i = 6; i < 10; i++) {
|
||||
rootNote.child(note(String(i)));
|
||||
}
|
||||
|
||||
const expectedOrder = ["top", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "bottom"];
|
||||
const expectedOrder = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ];
|
||||
|
||||
// Sort a few times to ensure that the resulting order is the same.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
@@ -97,12 +78,12 @@ describe("Tree", () => {
|
||||
it("pins to the top and bottom", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "5"},
|
||||
{title: "3"},
|
||||
{title: "2"},
|
||||
{title: "1"},
|
||||
{title: "top", "#top": ""}
|
||||
{ title: "bottom", "#bottom": "" },
|
||||
{ title: "5" },
|
||||
{ title: "3" },
|
||||
{ title: "2" },
|
||||
{ title: "1" },
|
||||
{ title: "top", "#top": "" }
|
||||
],
|
||||
"#sorted": ""
|
||||
});
|
||||
@@ -110,18 +91,18 @@ describe("Tree", () => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "5", "bottom"]);
|
||||
expect(orderedTitles).toStrictEqual([ "top", "1", "2", "3", "5", "bottom" ]);
|
||||
});
|
||||
|
||||
it("pins to the top and bottom in reverse order", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "3"},
|
||||
{title: "5"},
|
||||
{title: "top", "#top": ""}
|
||||
{ title: "bottom", "#bottom": "" },
|
||||
{ title: "1" },
|
||||
{ title: "2" },
|
||||
{ title: "3" },
|
||||
{ title: "5" },
|
||||
{ title: "top", "#top": "" }
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortDirection": "desc"
|
||||
@@ -130,50 +111,6 @@ describe("Tree", () => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "5", "3", "2", "1", "bottom"]);
|
||||
expect(orderedTitles).toStrictEqual([ "top", "5", "3", "2", "1", "bottom" ]);
|
||||
});
|
||||
|
||||
it("keeps folder notes on top when #sortFolderFirst is set, but not above #top", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "p1", children: [{title: "1.1"}, {title: "1.2"}]},
|
||||
{title: "p2", children: [{title: "2.1"}, {title: "2.2"}]},
|
||||
{title: "3"},
|
||||
{title: "5"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortFoldersFirst": ""
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "p1", "p2", "1", "2", "3", "5", "bottom"]);
|
||||
});
|
||||
|
||||
it("sorts notes accordingly when #sortNatural is set", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "10"},
|
||||
{title: "20"},
|
||||
{title: "3"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortNatural": ""
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "10", "20", "bottom"]);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
@@ -98,6 +98,15 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
||||
}
|
||||
|
||||
notes.sort((a, b) => {
|
||||
if (foldersFirst) {
|
||||
const aHasChildren = a.hasChildren();
|
||||
const bHasChildren = b.hasChildren();
|
||||
|
||||
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
|
||||
// exactly one note of the two is a directory, so the sorting will be done based on this status
|
||||
return aHasChildren ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchValue(note: BNote, key: string) {
|
||||
let rawValue: string | null;
|
||||
@@ -145,16 +154,6 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
||||
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);
|
||||
}
|
||||
|
||||
if (foldersFirst) {
|
||||
const aHasChildren = a.hasChildren();
|
||||
const bHasChildren = b.hasChildren();
|
||||
|
||||
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
|
||||
// exactly one note of the two is a directory, so the sorting will be done based on this status
|
||||
return aHasChildren ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
const customAEl = fetchValue(a, customSortBy) ?? fetchValue(a, "title") as string;
|
||||
const customBEl = fetchValue(b, customSortBy) ?? fetchValue(b, "title") as string;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect,it } from "vitest";
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import utils from "./utils.js";
|
||||
|
||||
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
|
||||
@@ -121,7 +120,7 @@ describe("#toObject", () => {
|
||||
{ testPropA: "keyA", testPropB: "valueA" },
|
||||
{ testPropA: "keyB", testPropB: "valueB" }
|
||||
];
|
||||
const fn: TestListFn = (testListEntry: TestListEntry) => [ `${testListEntry.testPropA }_fn`, `${testListEntry.testPropB }_fn` ];
|
||||
const fn: TestListFn = (testListEntry: TestListEntry) => [ testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn" ];
|
||||
|
||||
const result = utils.toObject(testList, fn);
|
||||
expect(result).toStrictEqual({
|
||||
@@ -241,8 +240,8 @@ describe.todo("#quoteRegex", () => {});
|
||||
|
||||
describe.todo("#replaceAll", () => {});
|
||||
|
||||
describe("#removeFileExtension", () => {
|
||||
const testCases: TestCase<typeof utils.removeFileExtension>[] = [
|
||||
describe("#removeTextFileExtension", () => {
|
||||
const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
|
||||
[ "w/ 'test.md' it should strip '.md'", [ "test.md" ], "test" ],
|
||||
[ "w/ 'test.markdown' it should strip '.markdown'", [ "test.markdown" ], "test" ],
|
||||
[ "w/ 'test.html' it should strip '.html'", [ "test.html" ], "test" ],
|
||||
@@ -253,7 +252,7 @@ describe("#removeFileExtension", () => {
|
||||
testCases.forEach((testCase) => {
|
||||
const [ desc, fnParams, expected ] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.removeFileExtension(...fnParams);
|
||||
const result = utils.removeTextFileExtension(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
import chardet from "chardet";
|
||||
import crypto from "crypto";
|
||||
import escape from "escape-html";
|
||||
import { t } from "i18next";
|
||||
import mimeTypes from "mime-types";
|
||||
import { release as osRelease } from "os";
|
||||
import path from "path";
|
||||
import { generator } from "rand-token";
|
||||
import sanitize from "sanitize-filename";
|
||||
import stripBom from "strip-bom";
|
||||
import crypto from "crypto";
|
||||
import { generator } from "rand-token";
|
||||
import unescape from "unescape";
|
||||
|
||||
import log from "./log.js";
|
||||
import escape from "escape-html";
|
||||
import sanitize from "sanitize-filename";
|
||||
import mimeTypes from "mime-types";
|
||||
import path from "path";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
import log from "./log.js";
|
||||
import { t } from "i18next";
|
||||
import { release as osRelease } from "os";
|
||||
|
||||
const osVersion = osRelease().split('.').map(Number);
|
||||
|
||||
@@ -205,7 +204,7 @@ export function formatDownloadTitle(fileName: string, type: string | null, mime:
|
||||
return `${fileNameBase}${getExtension()}`;
|
||||
}
|
||||
|
||||
export function removeFileExtension(filePath: string) {
|
||||
export function removeTextFileExtension(filePath: string) {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
@@ -217,7 +216,6 @@ export function removeFileExtension(filePath: string) {
|
||||
case ".excalidraw":
|
||||
case ".mermaid":
|
||||
case ".mmd":
|
||||
case ".pdf":
|
||||
return filePath.substring(0, filePath.length - extension.length);
|
||||
default:
|
||||
return filePath;
|
||||
@@ -228,7 +226,7 @@ export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boo
|
||||
const trimmedNoteMeta = noteMeta?.title?.trim();
|
||||
if (trimmedNoteMeta) return trimmedNoteMeta;
|
||||
|
||||
const basename = path.basename(removeFileExtension(filePath));
|
||||
const basename = path.basename(removeTextFileExtension(filePath));
|
||||
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
|
||||
}
|
||||
|
||||
@@ -469,28 +467,28 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined
|
||||
|
||||
// If already ends with slash, create both versions
|
||||
if (basePattern.endsWith('/')) {
|
||||
const withoutSlash = `${basePattern.slice(0, -1) }$`;
|
||||
const withoutSlash = basePattern.slice(0, -1) + '$';
|
||||
const withSlash = pattern;
|
||||
return [withoutSlash, withSlash];
|
||||
} else {
|
||||
// Add optional trailing slash
|
||||
const withSlash = basePattern + '/?$';
|
||||
return [withSlash];
|
||||
}
|
||||
// Add optional trailing slash
|
||||
const withSlash = `${basePattern }/?$`;
|
||||
return [withSlash];
|
||||
|
||||
}
|
||||
|
||||
// For patterns without $, add both versions
|
||||
if (pattern.endsWith('/')) {
|
||||
const withoutSlash = pattern.slice(0, -1);
|
||||
return [withoutSlash, pattern];
|
||||
} else {
|
||||
const withSlash = pattern + '/';
|
||||
return [pattern, withSlash];
|
||||
}
|
||||
const withSlash = `${pattern }/`;
|
||||
return [pattern, withSlash];
|
||||
|
||||
}
|
||||
|
||||
export function formatUtcTime(time: string) {
|
||||
return time.replace("T", " ").substring(0, 19);
|
||||
return time.replace("T", " ").substring(0, 19)
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with client utils
|
||||
@@ -503,9 +501,9 @@ export function formatSize(size: number | null | undefined) {
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
} else {
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
}
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
|
||||
}
|
||||
|
||||
function slugify(text: string) {
|
||||
@@ -546,7 +544,7 @@ export default {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
removeDiacritic,
|
||||
removeFileExtension,
|
||||
removeTextFileExtension,
|
||||
replaceAll,
|
||||
safeExtractMessageAndStackFromError,
|
||||
sanitizeSqlIdentifier,
|
||||
|
||||
@@ -16,28 +16,45 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
|
||||
// Prevent the window being garbage collected
|
||||
let mainWindow: BrowserWindow | null;
|
||||
let setupWindow: BrowserWindow | null;
|
||||
let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow) {
|
||||
interface WindowEntry {
|
||||
window: BrowserWindow;
|
||||
windowId: string; // custom window ID
|
||||
}
|
||||
let allWindowEntries: WindowEntry[] = [];
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow, windowId: string) {
|
||||
// We need to get the last focused window from allWindows. If the last window is closed, we return the previous window.
|
||||
// Therefore, we need to push the window into the allWindows array every time it gets focused.
|
||||
win.on("focus", () => {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
|
||||
allWindows.push(win);
|
||||
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
|
||||
allWindowEntries.push({ window: win, windowId: windowId });
|
||||
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed());
|
||||
cls.wrap(() => {
|
||||
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
|
||||
|
||||
const win = savedWindows.find(w => w.windowId === windowId);
|
||||
if (win) {
|
||||
win.closedAt = Date.now();
|
||||
}
|
||||
|
||||
optionService.setOption("openNoteContexts", JSON.stringify(savedWindows));
|
||||
})();
|
||||
|
||||
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed());
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createExtraWindow(extraWindowHash: string) {
|
||||
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
|
||||
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
|
||||
|
||||
const { BrowserWindow } = await import("electron");
|
||||
@@ -56,15 +73,15 @@ async function createExtraWindow(extraWindowHash: string) {
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`);
|
||||
|
||||
configureWebContents(win.webContents, spellcheckEnabled);
|
||||
|
||||
trackWindowFocus(win);
|
||||
trackWindowFocus(win, extraWindowId);
|
||||
}
|
||||
|
||||
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
|
||||
});
|
||||
|
||||
interface PrintOpts {
|
||||
@@ -168,8 +185,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
||||
return { browserWindow, printReport };
|
||||
}
|
||||
|
||||
async function createMainWindow(app: App) {
|
||||
if ("setUserTasks" in app) {
|
||||
async function createMainWindow(app?: App) {
|
||||
if (app && "setUserTasks" in app) {
|
||||
app.setUserTasks([
|
||||
{
|
||||
program: process.execPath,
|
||||
@@ -219,7 +236,7 @@ async function createMainWindow(app: App) {
|
||||
mainWindow.on("closed", () => (mainWindow = null));
|
||||
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
trackWindowFocus(mainWindow);
|
||||
trackWindowFocus(mainWindow, "main");
|
||||
}
|
||||
|
||||
function getWindowExtraOpts() {
|
||||
@@ -381,11 +398,15 @@ function getMainWindow() {
|
||||
}
|
||||
|
||||
function getLastFocusedWindow() {
|
||||
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
|
||||
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
|
||||
}
|
||||
|
||||
function getAllWindows() {
|
||||
return allWindows;
|
||||
return allWindowEntries.map(e => e.window);
|
||||
}
|
||||
|
||||
function getAllWindowIds(): string[] {
|
||||
return allWindowEntries.map(e => e.windowId);
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -396,5 +417,6 @@ export default {
|
||||
registerGlobalShortcuts,
|
||||
getMainWindow,
|
||||
getLastFocusedWindow,
|
||||
getAllWindows
|
||||
getAllWindows,
|
||||
getAllWindowIds
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getNoteIcon, NoteType } from "@triliumnext/commons";
|
||||
import escape from "escape-html";
|
||||
|
||||
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
||||
import type { Blob } from "../../../services/blob-interface.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import sql from "../../sql.js";
|
||||
@@ -19,7 +19,7 @@ const isCredentials = (attr: SAttribute) => attr.type === "label" && attr.name =
|
||||
class SNote extends AbstractShacaEntity {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: NoteType;
|
||||
type: string;
|
||||
mime: string;
|
||||
private blobId: string;
|
||||
utcDateModified: string;
|
||||
@@ -38,7 +38,7 @@ class SNote extends AbstractShacaEntity {
|
||||
|
||||
this.noteId = noteId;
|
||||
this.title = isProtected ? "[protected]" : title;
|
||||
this.type = type as NoteType;
|
||||
this.type = type;
|
||||
this.mime = mime;
|
||||
this.blobId = blobId;
|
||||
this.utcDateModified = utcDateModified; // used for caching of images
|
||||
@@ -528,22 +528,33 @@ class SNote extends AbstractShacaEntity {
|
||||
}
|
||||
|
||||
getIcon(filterByPrefix: string[] = []) {
|
||||
return `tn-icon ${this.#getIconInternal(filterByPrefix)}`;
|
||||
}
|
||||
|
||||
#getIconInternal(filterByPrefix: string[] = []) {
|
||||
const iconClassLabels = this.getLabels("iconClass").filter(label => {
|
||||
if (filterByPrefix.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return filterByPrefix.some(prefix => label.value.startsWith(prefix));
|
||||
});
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
workspaceIconClass: undefined,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
|
||||
return `tn-icon ${icon}`;
|
||||
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];
|
||||
}
|
||||
|
||||
isFolder() {
|
||||
|
||||