Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Commit 5fc0a04

Browse files
committed
Merge branch 'develop' into note-create
2 parents bcc689c + 4eb6435 commit 5fc0a04

File tree

117 files changed

+2484
-1660
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+2484
-1660
lines changed

.gitignore

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
22

3-
# Workaround for Nx bug: parent .gitignore files with '*' can cause
4-
# `nx show projects` to return nothing by ignoring subprojects.
5-
# See: https://github.com/nrwl/nx/issues/27368
6-
# Unignore everything to ensure Nx detects all projects
7-
!*
8-
93
# compiled output
104
dist
115
tmp

apps/client/.env.production

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
VITE_CKEDITOR_ENABLE_INSPECTOR=false
1+
VITE_CKEDITOR_ENABLE_INSPECTOR=false
2+
3+
# The development license key for premium CKEditor features.
4+
# Note: This key must only be used for the Trilium Notes project.
5+
# Expires on: 2025-09-13
6+
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w

apps/client/src/components/app_context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export type CommandMappings = {
281281
buildIcon(name: string): NativeImage;
282282
};
283283
refreshTouchBar: CommandData;
284+
reloadTextEditor: CommandData;
284285
};
285286

286287
type EventMappings = {

apps/client/src/services/froca.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ class FrocaImpl implements Froca {
245245
}
246246

247247
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
248+
if (noteIds.length === 0) {
249+
return [];
250+
}
251+
248252
noteIds = Array.from(new Set(noteIds)); // make unique
249253
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);
250254

apps/client/src/services/note_types.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { t } from "./i18n.js";
44
import type { MenuItem } from "../menus/context_menu.js";
55
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
66

7+
const SEPARATOR = { title: "----" };
8+
79
async function getNoteTypeItems(command?: TreeCommandNames) {
810
const items: MenuItem<TreeCommandNames>[] = [
911
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
@@ -18,25 +20,59 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
1820
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
1921
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
2022
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
23+
...await getBuiltInTemplates(command),
24+
...await getUserTemplates(command)
2125
];
2226

27+
return items;
28+
}
29+
30+
async function getUserTemplates(command?: TreeCommandNames) {
2331
const templateNoteIds = await server.get<string[]>("search-templates");
2432
const templateNotes = await froca.getNotes(templateNoteIds);
33+
if (templateNotes.length === 0) {
34+
return [];
35+
}
36+
37+
const items: MenuItem<TreeCommandNames>[] = [
38+
SEPARATOR
39+
];
40+
for (const templateNote of templateNotes) {
41+
items.push({
42+
title: templateNote.title,
43+
uiIcon: templateNote.getIcon(),
44+
command: command,
45+
type: templateNote.type,
46+
templateNoteId: templateNote.noteId
47+
});
48+
}
49+
return items;
50+
}
51+
52+
async function getBuiltInTemplates(command?: TreeCommandNames) {
53+
const templatesRoot = await froca.getNote("_templates");
54+
if (!templatesRoot) {
55+
console.warn("Unable to find template root.");
56+
return [];
57+
}
2558

26-
if (templateNotes.length > 0) {
27-
items.push({ title: "----" });
28-
29-
for (const templateNote of templateNotes) {
30-
items.push({
31-
title: templateNote.title,
32-
uiIcon: templateNote.getIcon(),
33-
command: command,
34-
type: templateNote.type,
35-
templateNoteId: templateNote.noteId
36-
});
37-
}
59+
const childNotes = await templatesRoot.getChildNotes();
60+
if (childNotes.length === 0) {
61+
return [];
3862
}
3963

64+
const items: MenuItem<TreeCommandNames>[] = [
65+
SEPARATOR
66+
];
67+
for (const templateNote of childNotes) {
68+
items.push({
69+
title: templateNote.title,
70+
uiIcon: templateNote.getIcon(),
71+
command: command,
72+
type: templateNote.type,
73+
templateNoteId: templateNote.noteId
74+
});
75+
}
4076
return items;
4177
}
4278

apps/client/src/stylesheets/style.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,16 +1280,19 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
12801280
padding: 0.5em 1em !important;
12811281
}
12821282

1283-
.ck.ck-slash-command-button__text-part {
1283+
.ck.ck-slash-command-button__text-part,
1284+
.ck.ck-template-form__text-part {
12841285
margin-left: 0.5em;
12851286
line-height: 1.2em !important;
12861287
}
12871288

1288-
.ck.ck-slash-command-button__text-part > span {
1289+
.ck.ck-slash-command-button__text-part > span,
1290+
.ck.ck-template-form__text-part > span {
12891291
line-height: inherit !important;
12901292
}
12911293

1292-
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description {
1294+
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description,
1295+
.ck.ck-template-form__text-part .ck-template-form__description {
12931296
display: block;
12941297
opacity: 0.8;
12951298
}

apps/client/src/types-assets.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ declare module "*?url" {
88
export default path;
99
}
1010

11+
declare module "*?raw" {
12+
var content: string;
13+
export default content;
14+
}
15+
1116
declare module "boxicons/css/boxicons.min.css" { }

apps/client/src/widgets/type_widgets/ckeditor/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../
77
import utils from "../../../services/utils.js";
88
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
99
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
10+
import getTemplates from "./snippets.js";
1011

1112
const TEXT_FORMATTING_GROUP = {
1213
label: "Text formatting",
1314
icon: "text"
1415
};
1516

16-
export function buildConfig(): EditorConfig {
17+
export async function buildConfig(): Promise<EditorConfig> {
1718
return {
1819
image: {
1920
styles: {
@@ -126,6 +127,9 @@ export function buildConfig(): EditorConfig {
126127
dropdownLimit: Number.MAX_SAFE_INTEGER,
127128
extraCommands: buildExtraCommands()
128129
},
130+
template: {
131+
definitions: await getTemplates()
132+
},
129133
// This value must be kept in sync with the language defined in webpack.config.js.
130134
language: "en"
131135
};
@@ -206,6 +210,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
206210
"outdent",
207211
"indent",
208212
"|",
213+
"insertTemplate",
209214
"markdownImport",
210215
"cuttonote",
211216
"findAndReplace"
@@ -262,6 +267,7 @@ export function buildFloatingToolbar() {
262267
"outdent",
263268
"indent",
264269
"|",
270+
"insertTemplate",
265271
"imageUpload",
266272
"markdownImport",
267273
"specialCharacters",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import debounce from "debounce";
2+
import froca from "../../../services/froca.js";
3+
import type LoadResults from "../../../services/load_results.js";
4+
import search from "../../../services/search.js";
5+
import type { TemplateDefinition } from "@triliumnext/ckeditor5";
6+
import appContext from "../../../components/app_context.js";
7+
import TemplateIcon from "@ckeditor/ckeditor5-icons/theme/icons/template.svg?raw";
8+
import type FNote from "../../../entities/fnote.js";
9+
10+
interface TemplateData {
11+
title: string;
12+
description?: string;
13+
content?: string;
14+
}
15+
16+
let templateCache: Map<string, TemplateData> = new Map();
17+
const debouncedHandleContentUpdate = debounce(handleContentUpdate, 1000);
18+
19+
/**
20+
* Generates the list of snippets based on the user's notes to be passed down to the CKEditor configuration.
21+
*
22+
* @returns the list of templates.
23+
*/
24+
export default async function getTemplates() {
25+
// Build the definitions and populate the cache.
26+
const snippets = await search.searchForNotes("#textSnippet");
27+
const definitions: TemplateDefinition[] = [];
28+
for (const snippet of snippets) {
29+
const { description } = await invalidateCacheFor(snippet);
30+
31+
definitions.push({
32+
title: snippet.title,
33+
data: () => templateCache.get(snippet.noteId)?.content ?? "",
34+
icon: TemplateIcon,
35+
description
36+
});
37+
}
38+
return definitions;
39+
}
40+
41+
async function invalidateCacheFor(snippet: FNote) {
42+
const description = snippet.getLabelValue("textSnippetDescription");
43+
const data: TemplateData = {
44+
title: snippet.title,
45+
description: description ?? undefined,
46+
content: await snippet.getContent()
47+
};
48+
templateCache.set(snippet.noteId, data);
49+
return data;
50+
}
51+
52+
function handleFullReload() {
53+
console.warn("Full text editor reload needed");
54+
appContext.triggerCommand("reloadTextEditor");
55+
}
56+
57+
async function handleContentUpdate(affectedNoteIds: string[]) {
58+
const updatedNoteIds = new Set(affectedNoteIds);
59+
const templateNoteIds = new Set(templateCache.keys());
60+
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
61+
62+
await froca.getNotes(affectedNoteIds);
63+
64+
let fullReloadNeeded = false;
65+
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
66+
try {
67+
const template = await froca.getNote(affectedTemplateNoteId);
68+
if (!template) {
69+
console.warn("Unable to obtain template with ID ", affectedTemplateNoteId);
70+
continue;
71+
}
72+
73+
const newTitle = template.title;
74+
if (templateCache.get(affectedTemplateNoteId)?.title !== newTitle) {
75+
fullReloadNeeded = true;
76+
break;
77+
}
78+
79+
await invalidateCacheFor(template);
80+
} catch (e) {
81+
// If a note was not found while updating the cache, it means we need to do a full reload.
82+
fullReloadNeeded = true;
83+
}
84+
}
85+
86+
if (fullReloadNeeded) {
87+
handleFullReload();
88+
}
89+
}
90+
91+
export function updateTemplateCache(loadResults: LoadResults): boolean {
92+
const affectedNoteIds = loadResults.getNoteIds();
93+
94+
// React to creation or deletion of text snippets.
95+
if (loadResults.getAttributeRows().find((attr) =>
96+
attr.type === "label" &&
97+
(attr.name === "textSnippet" || attr.name === "textSnippetDescription"))) {
98+
handleFullReload();
99+
} else if (affectedNoteIds.length > 0) {
100+
// Update content and titles if one of the template notes were updated.
101+
debouncedHandleContentUpdate(affectedNoteIds);
102+
}
103+
104+
return false;
105+
}

apps/client/src/widgets/type_widgets/editable_text.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getMermaidConfig } from "../../services/mermaid.js";
1818
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
1919
import "@triliumnext/ckeditor5/index.css";
2020
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
21+
import { updateTemplateCache } from "./ckeditor/snippets.js";
2122

2223
const mentionSetup: MentionFeed[] = [
2324
{
@@ -193,7 +194,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
193194

194195
const finalConfig = {
195196
...editorConfig,
196-
...buildConfig(),
197+
...(await buildConfig()),
197198
...buildToolbarConfig(isClassicEditor),
198199
htmlSupport: {
199200
allow: JSON.parse(options.get("allowedHtmlTags")),
@@ -326,7 +327,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
326327
const data = blob?.content || "";
327328
const newContentLanguage = this.note?.getLabelValue("language");
328329
if (this.contentLanguage !== newContentLanguage) {
329-
await this.reinitialize(data);
330+
await this.reinitializeWithData(data);
330331
} else {
331332
this.watchdog.editor?.setData(data);
332333
}
@@ -562,7 +563,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
562563
this.refreshIncludedNote(this.$editor, noteId);
563564
}
564565

565-
async reinitialize(data: string) {
566+
async reinitializeWithData(data: string) {
566567
if (!this.watchdog) {
567568
return;
568569
}
@@ -572,9 +573,25 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
572573
this.watchdog.editor?.setData(data);
573574
}
574575

575-
async onLanguageChanged() {
576+
async reinitialize() {
576577
const data = this.watchdog.editor?.getData();
577-
await this.reinitialize(data ?? "");
578+
await this.reinitializeWithData(data ?? "");
579+
}
580+
581+
async reloadTextEditorEvent() {
582+
await this.reinitialize();
583+
}
584+
585+
async onLanguageChanged() {
586+
await this.reinitialize();
587+
}
588+
589+
async entitiesReloadedEvent(e: EventData<"entitiesReloaded">) {
590+
await super.entitiesReloadedEvent(e);
591+
592+
if (updateTemplateCache(e.loadResults)) {
593+
await this.reinitialize();
594+
}
578595
}
579596

580597
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {

0 commit comments

Comments
 (0)