Compare commits

...

48 Commits

Author SHA1 Message Date
Elian Doran
f729e5b379 style(lexical): improve toolbar 2026-03-20 18:30:56 +02:00
Elian Doran
6e8c4813d9 fix(lexical): toolbar icon alignment 2026-03-20 18:28:51 +02:00
Elian Doran
029e8469b4 feat(lexical): add icons to toolbar and basic styling 2026-03-20 18:27:28 +02:00
Elian Doran
715b87fb3d chore(lexical): start working on a toolbar 2026-03-20 18:11:43 +02:00
Elian Doran
3d860ba2c4 feat(lexical): integrate scroll to end 2026-03-20 18:05:40 +02:00
Elian Doran
8b479ac4ba fix(lexical): unnecessary outline 2026-03-20 18:03:04 +02:00
Elian Doran
dcfcc7a334 fix(lexical): missing margin 2026-03-20 18:02:07 +02:00
Elian Doran
daa0109f7f fix(lexical): placeholder display issue 2026-03-20 18:00:47 +02:00
Elian Doran
f46e20aa25 feat(lexical): clear history when switching notes 2026-03-20 17:54:08 +02:00
Elian Doran
e8045424b2 feat(lexical): basic read 2026-03-20 17:51:23 +02:00
Elian Doran
7e6b628f86 feat(lexical): write without reading 2026-03-20 17:43:32 +02:00
Elian Doran
fc247fe790 feat(lexical): add very basic integration 2026-03-20 17:34:29 +02:00
Elian Doran
7a2fa2e829 feat(lexical): switch editable text based on MIME 2026-03-20 17:25:20 +02:00
Elian Doran
3e3f455b24 feat(lexical): integrate different MIME type for text 2026-03-20 17:17:44 +02:00
Elian Doran
55dea474e9 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.2.0 (#9099) 2026-03-20 13:45:51 +02:00
Elian Doran
bc74455a64 chore(deps): update dependency @smithy/middleware-retry to v4.4.44 (#9111) 2026-03-20 13:45:21 +02:00
Elian Doran
2d0b28367f chore(deps): update dependency vite to v8.0.1 (#9112) 2026-03-20 13:45:00 +02:00
Elian Doran
7d8a3e2811 fix(deps): update dependency katex to v0.16.39 (#9114) 2026-03-20 13:44:32 +02:00
renovate[bot]
79e5d9595a chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.2.0 2026-03-20 00:11:04 +00:00
renovate[bot]
0310626025 fix(deps): update dependency katex to v0.16.39 2026-03-20 00:08:50 +00:00
renovate[bot]
fefbb40c03 chore(deps): update dependency vite to v8.0.1 2026-03-20 00:07:33 +00:00
renovate[bot]
12f89078b8 chore(deps): update dependency @smithy/middleware-retry to v4.4.44 2026-03-20 00:06:57 +00:00
Elian Doran
8d873c5869 fix(share): prevent crash when accessing /share on uninitialized setup (#9088) 2026-03-19 20:13:32 +02:00
Elian Doran
27f4ac1d03 Textarea Label Support (#9077) 2026-03-19 20:02:11 +02:00
Elian Doran
d533360903 chore(attributes): rename textarea to multiline text 2026-03-19 20:01:55 +02:00
Elian Doran
49f5dc1c26 fix(table): text jumping when editing multiline text 2026-03-19 20:00:43 +02:00
Elian Doran
16419ed4ac Merge remote-tracking branch 'origin/main' into textarea-labels 2026-03-19 19:54:16 +02:00
Elian Doran
1595d1b5c9 Fix setup page error (#9102) 2026-03-19 19:50:07 +02:00
Elian Doran
50eb11997c fix(setup): contrast issue in alert (closes #8915) 2026-03-19 19:49:32 +02:00
Elian Doran
d974dfbc31 Translations update from Hosted Weblate (#9109) 2026-03-19 19:26:35 +02:00
Hosted Weblate
e9b63e50d4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-03-19 18:08:13 +01:00
Elian Doran
b6efb7c9ab feat(relation_map): add common note opening options to context menu 2026-03-19 19:07:54 +02:00
Elian Doran
f84479b1c2 fix(deps): update univer monorepo to v0.18.0 (#9101) 2026-03-19 17:48:23 +02:00
JYC333
8597fa560b Merge branch 'main' into fix/share-uninitialized-crash 2026-03-19 11:49:55 +00:00
JYC333
b29ab93fd5 fix: limit the scope of DB check only for share page 2026-03-19 11:46:35 +00:00
JYC333
225cdaff46 Update apps/client/src/setup.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-19 11:34:53 +00:00
JYC333
61dfba8c32 fix: remove knockout dependency 2026-03-19 11:24:45 +00:00
JYC333
4fee91d219 fix: change html to use DOM status 2026-03-19 11:24:09 +00:00
JYC333
12e4f76a8b fix: move setup to DOM implementation to fix knockout issue 2026-03-19 11:23:06 +00:00
JYC333
ec30598397 chore(deps): update dependency eslint-plugin-playwright to v2.10.1 (#9097) 2026-03-19 10:55:01 +00:00
JYC333
fdd2cc77e6 Merge branch 'main' into renovate/eslint-plugin-playwright-2.x 2026-03-19 10:30:16 +00:00
renovate[bot]
40ab2bc798 fix(deps): update univer monorepo to v0.18.0 2026-03-19 00:46:10 +00:00
renovate[bot]
bc907ee6ad chore(deps): update dependency eslint-plugin-playwright to v2.10.1 2026-03-19 00:43:25 +00:00
argusagent
e9987b40e6 fix(share): wire assertShareDbReady into router middleware — address code review 2026-03-17 20:27:04 -04:00
argusagent
1990a990c3 fix(share): return 503 when app is still initializing (#5677) 2026-03-17 20:22:06 -04:00
argusagent
d1159d3af9 fix(share): guard against uninitialized DB connection on /share routes (#5677) 2026-03-17 20:22:05 -04:00
Mystler
2b94d96930 fix(labels): Code review issue 2026-03-16 18:21:08 +01:00
Mystler
5b5222b846 feat(labels): Add textarea label type with Table support 2026-03-16 17:07:37 +01:00
49 changed files with 2150 additions and 1643 deletions

View File

@@ -24,6 +24,7 @@
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/rrule": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@lexical/react": "0.42.0",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
@@ -35,14 +36,14 @@
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@univerjs/preset-sheets-conditional-formatting": "0.17.0",
"@univerjs/preset-sheets-core": "0.17.0",
"@univerjs/preset-sheets-data-validation": "0.17.0",
"@univerjs/preset-sheets-filter": "0.17.0",
"@univerjs/preset-sheets-find-replace": "0.17.0",
"@univerjs/preset-sheets-note": "0.17.0",
"@univerjs/preset-sheets-sort": "0.17.0",
"@univerjs/presets": "0.17.0",
"@univerjs/preset-sheets-conditional-formatting": "0.18.0",
"@univerjs/preset-sheets-core": "0.18.0",
"@univerjs/preset-sheets-data-validation": "0.18.0",
"@univerjs/preset-sheets-filter": "0.18.0",
"@univerjs/preset-sheets-find-replace": "0.18.0",
"@univerjs/preset-sheets-note": "0.18.0",
"@univerjs/preset-sheets-sort": "0.18.0",
"@univerjs/presets": "0.18.0",
"@zumer/snapdom": "2.5.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
@@ -58,10 +59,10 @@
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.38",
"knockout": "3.5.2",
"katex": "0.16.39",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"lexical": "0.42.0",
"mark.js": "8.11.1",
"marked": "17.0.4",
"mermaid": "11.13.0",
@@ -92,4 +93,4 @@
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.3.0"
}
}
}

View File

@@ -39,6 +39,7 @@ export interface MenuCommandItem<T> {
title: string;
command?: T;
type?: string;
mime?: string;
/**
* The icon to display in the menu item.
*

View File

@@ -288,7 +288,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
}
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
async selectMenuItemHandler({ command, type, mime, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (utils.isMobile()) {
@@ -305,6 +305,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
target: "after",
targetBranchId: this.node.data.branchId,
type,
mime,
isProtected,
templateNoteId
});
@@ -313,6 +314,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
type,
mime,
isProtected: this.node.data.isProtected,
templateNoteId
});

View File

@@ -26,6 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
{ type: "text", mime: "application/json", title: "Text (Lexical)", icon: "bx-note" },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
// Text notes group
@@ -97,6 +98,7 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
title: nt.title,
command,
type: nt.type,
mime: nt.mime,
uiIcon: `bx ${nt.icon}`,
badges: []
};

View File

@@ -1,4 +1,4 @@
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
export type LabelType = "text" | "textarea" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
@@ -17,7 +17,7 @@ function parse(value: string) {
for (const token of tokens) {
if (token === "promoted") {
defObj.isPromoted = true;
} else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
defObj.labelType = token as LabelType;
} else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token as Multiplicity;

View File

@@ -1,68 +1,107 @@
import "jquery";
import ko from "knockout";
import utils from "./services/utils.js";
// TriliumNextTODO: properly make use of below types
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
type SetupStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop" | "sync-from-server";
type SetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
class SetupModel {
syncInProgress: boolean;
step: ko.Observable<string>;
setupType: ko.Observable<string>;
setupNewDocument: ko.Observable<boolean>;
setupSyncFromDesktop: ko.Observable<boolean>;
setupSyncFromServer: ko.Observable<boolean>;
syncServerHost: ko.Observable<string | undefined>;
syncProxy: ko.Observable<string | undefined>;
password: ko.Observable<string | undefined>;
class SetupController {
private step: SetupStep;
private setupType: SetupType = "";
private syncPollIntervalId: number | null = null;
private rootNode: HTMLElement;
private setupTypeForm: HTMLFormElement;
private syncFromServerForm: HTMLFormElement;
private setupTypeNextButton: HTMLButtonElement;
private setupTypeInputs: HTMLInputElement[];
private syncServerHostInput: HTMLInputElement;
private syncProxyInput: HTMLInputElement;
private passwordInput: HTMLInputElement;
private sections: Record<SetupStep, HTMLElement>;
constructor(syncInProgress: boolean) {
this.syncInProgress = syncInProgress;
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
this.setupType = ko.observable("");
this.setupNewDocument = ko.observable(false);
this.setupSyncFromDesktop = ko.observable(false);
this.setupSyncFromServer = ko.observable(false);
this.syncServerHost = ko.observable();
this.syncProxy = ko.observable();
this.password = ko.observable();
constructor(rootNode: HTMLElement, syncInProgress: boolean) {
this.rootNode = rootNode;
this.step = syncInProgress ? "sync-in-progress" : "setup-type";
this.setupTypeForm = mustGetElement("setup-type-form", HTMLFormElement);
this.syncFromServerForm = mustGetElement("sync-from-server-form", HTMLFormElement);
this.setupTypeNextButton = mustGetElement("setup-type-next", HTMLButtonElement);
this.setupTypeInputs = Array.from(document.querySelectorAll<HTMLInputElement>("input[name='setup-type']"));
this.syncServerHostInput = mustGetElement("sync-server-host", HTMLInputElement);
this.syncProxyInput = mustGetElement("sync-proxy", HTMLInputElement);
this.passwordInput = mustGetElement("password", HTMLInputElement);
this.sections = {
"setup-type": mustGetElement("setup-type-section", HTMLElement),
"new-document-in-progress": mustGetElement("new-document-in-progress-section", HTMLElement),
"sync-from-desktop": mustGetElement("sync-from-desktop-section", HTMLElement),
"sync-from-server": mustGetElement("sync-from-server-section", HTMLElement),
"sync-in-progress": mustGetElement("sync-in-progress-section", HTMLElement)
};
}
if (this.syncInProgress) {
setInterval(checkOutstandingSyncs, 1000);
init() {
this.setupTypeForm.addEventListener("submit", (event) => {
event.preventDefault();
void this.selectSetupType();
});
this.syncFromServerForm.addEventListener("submit", (event) => {
event.preventDefault();
void this.finish();
});
for (const input of this.setupTypeInputs) {
input.addEventListener("change", () => {
this.setupType = input.value as SetupType;
this.render();
});
}
for (const backButton of document.querySelectorAll<HTMLElement>("[data-action='back']")) {
backButton.addEventListener("click", () => {
this.back();
});
}
const serverAddress = `${location.protocol}//${location.host}`;
$("#current-host").html(serverAddress);
if (this.step === "sync-in-progress") {
this.startSyncPolling();
}
this.render();
this.rootNode.style.display = "";
}
// this is called in setup.ejs
setupTypeSelected() {
return !!this.setupType();
}
private async selectSetupType() {
if (this.setupType === "new-document") {
this.setStep("new-document-in-progress");
selectSetupType() {
if (this.setupType() === "new-document") {
this.step("new-document-in-progress");
await $.post("api/setup/new-document");
window.location.replace("./setup");
return;
}
$.post("api/setup/new-document").then(() => {
window.location.replace("./setup");
});
} else {
this.step(this.setupType());
if (this.setupType) {
this.setStep(this.setupType);
}
}
back() {
this.step("setup-type");
this.setupType("");
private back() {
this.setStep("setup-type");
this.setupType = "";
for (const input of this.setupTypeInputs) {
input.checked = false;
}
this.render();
}
async finish() {
const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy();
const password = this.password();
private async finish() {
const syncServerHost = this.syncServerHostInput.value.trim();
const syncProxy = this.syncProxyInput.value.trim();
const password = this.passwordInput.value;
if (!syncServerHost) {
showAlert("Trilium server address can't be empty");
@@ -82,15 +121,38 @@ class SetupModel {
});
if (resp.result === "success") {
this.step("sync-in-progress");
setInterval(checkOutstandingSyncs, 1000);
hideAlert();
this.setStep("sync-in-progress");
this.startSyncPolling();
} else {
showAlert(`Sync setup failed: ${resp.error}`);
}
}
private setStep(step: SetupStep) {
this.step = step;
this.render();
}
private render() {
for (const [step, section] of Object.entries(this.sections) as [SetupStep, HTMLElement][]) {
section.style.display = step === this.step ? "" : "none";
}
this.setupTypeNextButton.disabled = !this.setupType;
}
private getSelectedSetupType(): SetupType {
return (this.setupTypeInputs.find((input) => input.checked)?.value ?? "") as SetupType;
}
private startSyncPolling() {
if (this.syncPollIntervalId !== null) {
return;
}
this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000);
}
}
async function checkOutstandingSyncs() {
@@ -124,9 +186,19 @@ function getSyncInProgress() {
return !!parseInt(el.content);
}
function mustGetElement<T extends typeof HTMLElement>(id: string, ctor: T): InstanceType<T> {
const element = document.getElementById(id);
if (!element || !(element instanceof ctor)) {
throw new Error(`Expected element #${id}`);
}
return element as InstanceType<T>;
}
addEventListener("DOMContentLoaded", (event) => {
const rootNode = document.getElementById("setup-dialog");
if (!rootNode) return;
ko.applyBindings(new SetupModel(getSyncInProgress()), rootNode);
$("#setup-dialog").show();
if (!rootNode || !(rootNode instanceof HTMLElement)) return;
new SetupController(rootNode, getSyncInProgress()).init();
});

View File

@@ -675,10 +675,11 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
div.alert {
margin-bottom: 8px;
background: var(--alert-bar-background) !important;
color: var(--main-text-color);
border-radius: 8px;
font-size: .85em;
}
div.alert p + p {
margin-block: 1em 0;
}
}

View File

@@ -1069,7 +1069,6 @@
"rename_note": "اعادة تسمية الملاحظة",
"remove_relation": "حذف العلاقة",
"default_new_note_title": "ملاحظة جديدة",
"open_in_new_tab": "فتح في تبويب جديد",
"enter_new_title": "ادخل عنوان ملاحظة جديدة:",
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"

View File

@@ -1047,7 +1047,6 @@
"unprotecting-title": "解除保护状态"
},
"relation_map": {
"open_in_new_tab": "在新标签页中打开",
"remove_note": "删除笔记",
"edit_title": "编辑标题",
"rename_note": "重命名笔记",

View File

@@ -1046,7 +1046,6 @@
"unprotecting-title": "Ungeschützt-Status"
},
"relation_map": {
"open_in_new_tab": "In neuem Tab öffnen",
"remove_note": "Notiz entfernen",
"edit_title": "Titel bearbeiten",
"rename_note": "Notiz umbenennen",

View File

@@ -343,6 +343,7 @@
"label_type_title": "Type of the label will help Trilium to choose suitable interface to enter the label value.",
"label_type": "Type",
"text": "Text",
"textarea": "Multi-line Text",
"number": "Number",
"boolean": "Boolean",
"date": "Date",
@@ -1068,7 +1069,6 @@
"unprotecting-title": "Unprotecting status"
},
"relation_map": {
"open_in_new_tab": "Open in new tab",
"remove_note": "Remove note",
"edit_title": "Edit title",
"rename_note": "Rename note",

View File

@@ -1051,7 +1051,6 @@
"unprotecting-title": "Estado de desprotección"
},
"relation_map": {
"open_in_new_tab": "Abrir en nueva pestaña",
"remove_note": "Quitar nota",
"edit_title": "Editar título",
"rename_note": "Cambiar nombre de nota",

View File

@@ -1036,7 +1036,6 @@
"unprotecting-title": "Statut de la non-protection"
},
"relation_map": {
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"remove_note": "Supprimer la note",
"edit_title": "Modifier le titre",
"rename_note": "Renommer la note",

View File

@@ -1055,7 +1055,6 @@
"unprotecting-title": "Stádas díchosanta"
},
"relation_map": {
"open_in_new_tab": "Oscail i gcluaisín nua",
"remove_note": "Bain nóta",
"edit_title": "Cuir an teideal in eagar",
"rename_note": "Athainmnigh an nóta",

View File

@@ -1049,7 +1049,6 @@
"unprotecting-title": "अन-प्रोटेक्ट स्टेटस"
},
"relation_map": {
"open_in_new_tab": "नए टैब में खोलें",
"remove_note": "नोट हटाएं",
"edit_title": "टाइटल एडिट करें",
"rename_note": "नोट का नाम बदलें",

View File

@@ -1424,7 +1424,6 @@
"unprotecting-title": "Stato non protetto"
},
"relation_map": {
"open_in_new_tab": "Apri in una nuova scheda",
"remove_note": "Rimuovi nota",
"edit_title": "Modifica titolo",
"rename_note": "Rinomina nota",

View File

@@ -1537,7 +1537,6 @@
"url_placeholder": "http://web サイト..."
},
"relation_map": {
"open_in_new_tab": "新しいタブで開く",
"remove_note": "ノートを削除",
"edit_title": "タイトルを編集",
"rename_note": "ノート名を変更",

View File

@@ -1275,7 +1275,6 @@
"unprotecting-title": "Status zdejmowania ochrony"
},
"relation_map": {
"open_in_new_tab": "Otwórz w nowej karcie",
"remove_note": "Usuń notatkę",
"edit_title": "Edytuj tytuł",
"rename_note": "Zmień nazwę notatki",

View File

@@ -1047,7 +1047,6 @@
"unprotecting-title": "Estado da remoção de proteção"
},
"relation_map": {
"open_in_new_tab": "Abrir em nova guia",
"remove_note": "Remover nota",
"edit_title": "Editar título",
"rename_note": "Renomear nota",

View File

@@ -1111,7 +1111,6 @@
"start_session_button": "Iniciar sessão protegida"
},
"relation_map": {
"open_in_new_tab": "Abrir em nova aba",
"remove_note": "Remover nota",
"edit_title": "Editar título",
"rename_note": "Renomear nota",

View File

@@ -1054,7 +1054,6 @@
"enter_title_of_new_note": "Introduceți titlul noii notițe",
"note_already_in_diagram": "Notița „{{title}}” deja se află pe diagramă.",
"note_not_found": "Notița „{{noteId}}” nu a putut fi găsită!",
"open_in_new_tab": "Deschide într-un tab nou",
"remove_note": "Șterge notița",
"remove_relation": "Șterge relația",
"rename_note": "Redenumește notița",

View File

@@ -1625,7 +1625,6 @@
"rename_note": "Переименовать заметку",
"remove_relation": "Удалить отношение",
"default_new_note_title": "новая заметка",
"open_in_new_tab": "Открыть в новой вкладке",
"confirm_remove_relation": "Вы уверены, что хотите удалить связь?",
"enter_new_title": "Введите новое название заметки:",
"note_not_found": "Заметка {{noteId}} не найдена!",

View File

@@ -1046,7 +1046,6 @@
"unprotecting-title": "解除保護狀態"
},
"relation_map": {
"open_in_new_tab": "在新分頁中打開",
"remove_note": "刪除筆記",
"edit_title": "編輯標題",
"rename_note": "重新命名筆記",

View File

@@ -1151,7 +1151,6 @@
"unprotecting-title": "Статус зняття захисту"
},
"relation_map": {
"open_in_new_tab": "Відкрити в новій вкладці",
"remove_note": "Видалити нотатку",
"edit_title": "Редагувати заголовок",
"rename_note": "Перейменувати нотатку",

View File

@@ -2,7 +2,7 @@ import "./PromotedAttributes.css";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { ComponentChild, createElement, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
@@ -36,7 +36,7 @@ interface CellProps {
setCellToFocus(cell: Cell): void;
}
type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeEventData = TargetedEvent<HTMLInputElement | HTMLTextAreaElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
export default function PromotedAttributes() {
@@ -171,8 +171,9 @@ function PromotedAttributeCell(props: CellProps) {
);
}
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute | undefined> = {
text: "text",
textarea: undefined,
number: "number",
boolean: "checkbox",
date: "date",
@@ -226,20 +227,21 @@ function LabelInput(props: CellProps & { inputId: string }) {
}
}
const inputNode = <input
className="form-control promoted-attribute-input"
tabIndex={200 + definitionAttr.position}
id={inputId}
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
value={valueDraft}
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
placeholder={t("promoted_attributes.unset-field-placeholder")}
data-attribute-id={valueAttr.attributeId}
data-attribute-type={valueAttr.type}
data-attribute-name={valueAttr.name}
onBlur={onChangeListener}
{...extraInputProps}
/>;
const inputNode = createElement(definition.labelType === "textarea" ? "textarea" : "input", {
className: "form-control promoted-attribute-input",
tabIndex: 200 + definitionAttr.position,
id: inputId,
type: LABEL_MAPPINGS[definition.labelType ?? "text"],
value: valueDraft,
checked: definition.labelType === "boolean" ? valueAttr.value === "true" : undefined,
placeholder: t("promoted_attributes.unset-field-placeholder"),
"data-attribute-id": valueAttr.attributeId,
"data-attribute-type": valueAttr.type,
"data-attribute-name": valueAttr.name,
onBlur: onChangeListener,
...extraInputProps
});
if (definition.labelType === "boolean") {
return <>

View File

@@ -137,6 +137,7 @@ const TPL = /*html*/`
<td>
<select class="attr-input-label-type form-control">
<option value="text">${t("attribute_detail.text")}</option>
<option value="textarea">${t("attribute_detail.textarea")}</option>
<option value="number">${t("attribute_detail.number")}</option>
<option value="boolean">${t("attribute_detail.boolean")}</option>
<option value="date">${t("attribute_detail.date")}</option>

View File

@@ -19,6 +19,13 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
text: {
editor: "input"
},
textarea: {
editor: "textarea",
formatter: "textarea",
editorParams: {
shiftEnterSubmit: true
}
},
boolean: {
formatter: "tickCross",
editor: "tickCross"

View File

@@ -75,3 +75,9 @@
font-size: 1.5em;
transform: translateY(-50%);
}
.tabulator .tabulator-editable {
textarea {
padding: 7px !important;
}
}

View File

@@ -1,12 +1,14 @@
import { Connection } from "jsplumb";
import { RefObject } from "preact";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
import contextMenu from "../../../menus/context_menu";
import link_context_menu from "../../../menus/link_context_menu";
import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import RelationMapApi from "./api";
import { Connection } from "jsplumb";
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
return (e: MouseEvent) => {
@@ -17,22 +19,8 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
x: e.pageX,
y: e.pageY,
items: [
{
title: t("relation_map.open_in_new_tab"),
uiIcon: "bx bx-empty",
handler: () => appContext.tabManager.openTabWithNoteWithHoisting(note.noteId)
},
{
title: t("relation_map.remove_note"),
uiIcon: "bx bx-trash",
handler: async () => {
if (!note) return;
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
if (typeof result !== "object" || !result.confirmed) return;
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
}
},
...link_context_menu.getItems(e),
{ kind: "separator" },
{
title: t("relation_map.edit_title"),
uiIcon: "bx bx-pencil",
@@ -49,10 +37,26 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
await server.put(`notes/${note.noteId}/title`, { title });
}
}
},
{ kind: "separator" },
{
title: t("relation_map.remove_note"),
uiIcon: "bx bx-trash",
handler: async () => {
if (!note) return;
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
if (typeof result !== "object" || !result.confirmed) return;
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
}
},
],
selectMenuItemHandler() {}
})
selectMenuItemHandler({ command }) {
// Pass the events to the link context menu
link_context_menu.handleLinkContextMenuItem(command, e, note.noteId);
}
});
};
}

View File

@@ -14,10 +14,11 @@ import note_create from "../../../services/note_create";
import options from "../../../services/options";
import toast from "../../../services/toast";
import utils, { hasTouchBar, isMobile } from "../../../services/utils";
import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
import { TypeWidgetProps } from "../type_widget";
import CKEditorWithWatchdog, { CKEditorApi } from "./CKEditorWithWatchdog";
import LexicalText from "./lexical";
import getTemplates, { updateTemplateCache } from "./snippets.js";
import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
@@ -27,7 +28,15 @@ import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./util
* - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph).
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/
export default function EditableText({ note, parentComponent, ntxId, noteContext }: TypeWidgetProps) {
export default function EditableText(props: TypeWidgetProps) {
const mime = useNoteProperty(props.note, "mime");
if (mime === "application/json") {
return <LexicalText {...props} />;
}
return <EditableTextCKEditor {...props} />;
}
function EditableTextCKEditor({ note, parentComponent, ntxId, noteContext }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<string>("");
const watchdogRef = useRef<EditorWatchdog>(null);

View File

@@ -0,0 +1,41 @@
.note-detail-editable-text {
.toolbar {
display: flex;
margin-bottom: 1px;
background: var(--classic-toolbar-vert-layout-background-color);
padding: 3px 6px;
border-radius: 6px;
margin: 20px;
vertical-align: middle;
}
.toolbar .divider {
width: 1px;
background-color: var(--main-border-color);
margin: 0 6px;
}
.toolbar .toolbar-item .text {
display: flex;
line-height: 20px;
width: 200px;
vertical-align: middle;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.toolbar .toolbar-item .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
}

View File

@@ -0,0 +1,177 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import "./ToolbarPlugin.css";
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_LOW,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';
import ActionButton, { ActionButtonProps } from "../../../react/ActionButton";
function Divider() {
return <div className="divider" />;
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const $updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
}
}, []);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(
() => {
$updateToolbar();
},
{editor},
);
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, _newEditor) => {
$updateToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, $updateToolbar]);
return (
<div className="toolbar" ref={toolbarRef}>
<ToolbarButton
disabled={!canUndo}
onClick={() => {
editor.dispatchCommand(UNDO_COMMAND, undefined);
}}
text="Undo"
icon="bx bx-undo"
/>
<ToolbarButton
disabled={!canRedo}
onClick={() => {
editor.dispatchCommand(REDO_COMMAND, undefined);
}}
text="Redo"
icon="bx bx-redo"
/>
<Divider />
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
active={isBold}
text="Format Bold"
icon="bx bx-bold"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
active={isItalic}
text="Format Italics"
icon="bx bx-italic"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
active={isUnderline}
text="Format Underline"
icon="bx bx-underline"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
active={isStrikethrough}
text="Format Strikethrough"
icon="bx bx-strikethrough"
/>
<Divider />
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
}}
text="Left Align"
icon="bx bx-align-left"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
}}
text="Center Align"
icon="bx bx-align-middle"
/>
<ToolbarButton
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')}
text="Right Align"
icon="bx bx-align-right"
/>
<ToolbarButton
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')}
text="Justify Align"
icon="bx bx-align-justify"
/>{' '}
</div>
);
}
function ToolbarButton(props: Pick<ActionButtonProps, "icon" | "disabled" | "onClick" | "text">) {
return (
<ActionButton
className="toolbar-item"
{...props}
/>
);
}

View File

@@ -0,0 +1,21 @@
.note-detail-editable-text .lexical-wrapper {
color: var(--main-text-color);
font-family: var(--main-font-family);
font-size: var(--main-font-size);
line-height: 1.5;
word-break: break-word;
position: relative;
margin-inline: var(--content-margin-inline);
>div[contenteditable="true"] {
outline: 0;
}
.lexical-placeholder {
opacity: 0.5;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
}

View File

@@ -0,0 +1,120 @@
import "./index.css";
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {$createRangeSelection, $getRoot, $setSelection, CLEAR_HISTORY_COMMAND} from 'lexical';
import { useEffect } from 'preact/hooks';
import { useEditorSpacedUpdate, useTriliumEvent } from '../../../react/hooks';
import { TypeWidgetProps } from "../../type_widget";
import ToolbarPlugin from "./ToolbarPlugin";
const theme = {
// Theme styling goes here
//...
};
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
console.error(error);
}
export default function LexicalText(props: TypeWidgetProps) {
const initialConfig = {
namespace: 'MyEditor',
theme,
onError,
};
const placeholder = (
<div className="lexical-placeholder">
Enter some text...
</div>
);
return (
<LexicalComposer initialConfig={initialConfig}>
<ToolbarPlugin />
<div className="lexical-wrapper">
<RichTextPlugin
contentEditable={<ContentEditable /> as never}
placeholder={placeholder as never}
ErrorBoundary={LexicalErrorBoundary}
/>
</div>
<HistoryPlugin />
<AutoFocusPlugin />
<ScrollToEndPlugin />
<CustomEditorPersistencePlugin {...props} />
</LexicalComposer>
);
}
function CustomEditorPersistencePlugin({ note, noteContext }: TypeWidgetProps) {
const [editor] = useLexicalComposerContext();
const spacedUpdate = useEditorSpacedUpdate({
note,
noteContext,
noteType: "text",
getData() {
return {
content: JSON.stringify(editor.toJSON().editorState)
};
},
onContentChange(newContent) {
if (!newContent) {
editor.update(() => {
$getRoot().clear();
});
return;
}
try {
const editorState = editor.parseEditorState(newContent);
editor.setEditorState(editorState);
} catch (err) {
console.error("Error parsing Lexical content", err);
}
},
});
// Clear the history whenever note changes.
useEffect(() => {
editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
}, [ editor, note ]);
// Detect changes in content.
useEffect(() => {
return editor.registerUpdateListener(() => {
spacedUpdate.scheduleUpdate();
});
}, [ spacedUpdate, editor ]);
}
function ScrollToEndPlugin() {
const [editor] = useLexicalComposerContext();
useTriliumEvent("scrollToEnd", () => {
editor.update(() => {
const root = $getRoot();
const lastChild = root.getLastDescendant();
if (lastChild) {
const selection = $createRangeSelection();
selection.anchor.set(lastChild.getKey(), lastChild.getTextContentSize(), 'text');
selection.focus.set(lastChild.getKey(), lastChild.getTextContentSize(), 'text');
$setSelection(selection);
}
});
editor.focus();
});
return null;
}

View File

@@ -6,6 +6,7 @@
"main": "./src/main.ts",
"scripts": {
"dev": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
"dev-alt": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data2 TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
"start-no-dir": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
"edit-integration-db": "cross-env NODE_ENV=development TRILIUM_PORT=8086 TRILIUM_ENV=dev TRILIUM_DATA_DIR=spec/db TRILIUM_INTEGRATION_TEST=edit TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
"build": "tsx scripts/build.ts",
@@ -125,7 +126,7 @@
"tmp": "0.2.5",
"turnish": "1.8.0",
"unescape": "1.0.1",
"vite": "8.0.0",
"vite": "8.0.1",
"ws": "8.19.0",
"xml2js": "0.6.2",
"yauzl": "3.2.1"

View File

@@ -58,35 +58,35 @@
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<form data-bind="submit: selectSetupType">
<div id="setup-type-section" style="margin-top: 20px;">
<form id="setup-type-form">
<div class="radio" style="margin-bottom: 15px;">
<label class="tn-radio">
<input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
<input type="radio" name="setup-type" value="new-document">
<%= t("setup.new-document") %>
</label>
</div>
<div class="radio" style="margin-bottom: 15px;">
<label class="tn-radio">
<input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
<input type="radio" name="setup-type" value="sync-from-desktop">
<%= t("setup.sync-from-desktop") %>
</label>
</div>
<div class="radio" style="margin-bottom: 15px;">
<label class="tn-radio">
<input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
<input type="radio" name="setup-type" value="sync-from-server">
<%= t("setup.sync-from-server") %>
</label>
</div>
<button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button>
<button type="submit" id="setup-type-next" class="btn btn-primary" disabled><%= t("setup.next") %></button>
</form>
</div>
<div data-bind="visible: step() == 'new-document-in-progress'">
<div id="new-document-in-progress-section">
<h2><%= t("setup.init-in-progress") %></h2>
<div style="display: flex; justify-content: flex-start; margin-top: 20px;">
@@ -103,7 +103,7 @@
</div>
</div>
<div data-bind="visible: step() == 'sync-from-desktop'">
<div id="sync-from-desktop-section">
<h2><%= t("setup_sync-from-desktop.heading") %></h2>
<p><%= t("setup_sync-from-desktop.description") %></p>
@@ -117,11 +117,11 @@
<li><%- t("setup_sync-from-desktop.step6", { link: `<a href="/">${t("setup_sync-from-desktop.step6-here")}</a>` }) %></li>
</ol>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
<button type="button" data-action="back" class="btn btn-secondary">Back</button>
</div>
<div data-bind="visible: step() == 'sync-from-server'">
<form data-bind="submit: finish">
<div id="sync-from-server-section">
<form id="sync-from-server-form">
<h2><%= t("setup_sync-from-server.heading") %></h2>
@@ -129,27 +129,27 @@
<div class="form-group">
<label for="sync-server-host"><%= t("setup_sync-from-server.server-host") %></label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
<input type="text" id="sync-server-host" class="form-control" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
</div>
<div class="form-group">
<label for="sync-proxy"><%= t("setup_sync-from-server.proxy-server") %></label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
<input type="text" id="sync-proxy" class="form-control" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
<p><strong><%= t("setup_sync-from-server.note") %></strong> <%= t("setup_sync-from-server.proxy-instruction") %></p>
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label for="password"><%= t("setup_sync-from-server.password") %></label>
<input type="password" id="password" class="form-control" data-bind="value: password" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
<input type="password" id="password" class="form-control" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
</div>
<button type="button" data-bind="click: back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
<button type="button" data-action="back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
<button type="submit" class="btn btn-primary"><%= t("setup_sync-from-server.finish-setup") %></button>
</form>
</div>
<div data-bind="visible: step() == 'sync-in-progress'">
<div id="sync-in-progress-section">
<h2><%= t("setup_sync-in-progress.heading") %></h2>
<div class="alert alert-success"><%= t("setup_sync-in-progress.successful") %></div>

View File

@@ -7,7 +7,7 @@ function parse(value: string): DefinitionObject {
for (const token of tokens) {
if (token === "promoted") {
defObj.isPromoted = true;
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
defObj.labelType = token;
} else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token;

View File

@@ -1,6 +1,6 @@
import safeCompare from "safe-compare";
import type { Request, Response, Router } from "express";
import type { NextFunction, Request, Response, Router } from "express";
import shaca from "./shaca/shaca.js";
import shacaLoader from "./shaca/shaca_loader.js";
@@ -10,6 +10,16 @@ import type SNote from "./shaca/entities/snote.js";
import type SAttachment from "./shaca/entities/sattachment.js";
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
import utils from "../services/utils.js";
import { isShareDbReady } from "./sql.js";
function assertShareDbReady(_req: Request, res: Response, next: NextFunction) {
if (!isShareDbReady()) {
res.status(503).send("The application is still initializing. Please try again in a moment.");
return;
}
next();
}
function addNoIndexHeader(note: SNote, res: Response) {
if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
@@ -115,6 +125,8 @@ function render404(res: Response) {
}
function register(router: Router) {
// Guard: if the share DB is not yet initialized, return 503 for all /share routes.
router.use("/share", assertShareDbReady);
function renderNote(note: SNote, req: Request, res: Response) {
if (!note) {

View File

@@ -5,12 +5,14 @@ import dataDir from "../services/data_dir.js";
import sql_init from "../services/sql_init.js";
let dbConnection!: Database.Database;
let dbConnectionReady = false;
sql_init.dbReady.then(() => {
dbConnection = new Database(dataDir.DOCUMENT_PATH, {
readonly: true,
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
});
dbConnectionReady = true;
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
process.on(eventType, () => {
@@ -23,18 +25,31 @@ sql_init.dbReady.then(() => {
});
});
function assertDbReady(): void {
if (!dbConnectionReady) {
throw new Error("Share database connection is not yet ready. The application may still be initializing.");
}
}
function getRawRows<T>(query: string, params = []): T[] {
assertDbReady();
return dbConnection.prepare(query).raw().all(params) as T[];
}
function getRow<T>(query: string, params: string[] = []): T {
assertDbReady();
return dbConnection.prepare(query).get(params) as T;
}
function getColumn<T>(query: string, params: string[] = []): T[] {
assertDbReady();
return dbConnection.prepare(query).pluck().all(params) as T[];
}
export function isShareDbReady(): boolean {
return dbConnectionReady;
}
export default {
getRawRows,
getRow,

View File

@@ -22,7 +22,7 @@
"eslint-config-preact": "2.0.0",
"typescript": "5.9.3",
"user-agent-data-types": "0.4.2",
"vite": "8.0.0",
"vite": "8.0.1",
"vitest": "4.1.0"
},
"eslintConfig": {

View File

@@ -13,6 +13,7 @@
"server:build": "pnpm run --filter server build",
"server:coverage": "pnpm run --filter server test --coverage",
"server:start": "pnpm run --filter server dev",
"server:start-alt": "pnpm run --filter server dev-alt",
"server:start-prod": "pnpm run --filter server start-prod",
"desktop:start": "pnpm run --filter desktop dev",
"desktop:build": "pnpm run --filter desktop build",
@@ -61,7 +62,7 @@
"eslint": "10.0.3",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.10.0",
"eslint-plugin-playwright": "2.10.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.8.4",
"http-server": "14.1.1",
@@ -75,7 +76,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.57.1",
"upath": "2.0.1",
"vite": "8.0.0",
"vite": "8.0.1",
"vite-plugin-dts": "4.5.4",
"vitest": "4.1.0"
},

View File

@@ -21,7 +21,7 @@
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.1",

View File

@@ -22,7 +22,7 @@
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.1",

View File

@@ -24,7 +24,7 @@
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.1",

View File

@@ -24,7 +24,7 @@
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.1",

View File

@@ -24,7 +24,7 @@
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.1",

View File

@@ -16,7 +16,7 @@
"ckeditor5-premium-features": "47.6.1"
},
"devDependencies": {
"@smithy/middleware-retry": "4.4.43",
"@smithy/middleware-retry": "4.4.44",
"@types/jquery": "4.0.0"
}
}

View File

@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"dependencies": {
"fuse.js": "7.1.0",
"katex": "0.16.38",
"katex": "0.16.39",
"mermaid": "11.13.0"
},
"devDependencies": {

3007
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff