mirror of
https://github.com/zadam/trilium.git
synced 2026-01-25 00:29:13 +01:00
Compare commits
68 Commits
week-note
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7daee771 | ||
|
|
50aeda8ee8 | ||
|
|
1520c696a3 | ||
|
|
f63f6244a1 | ||
|
|
8611d4a67a | ||
|
|
c48bd9a5c3 | ||
|
|
dba985b308 | ||
|
|
a51a831fe8 | ||
|
|
44142e980d | ||
|
|
7f83226f84 | ||
|
|
e3fdae8932 | ||
|
|
78c62be823 | ||
|
|
e51cea88bf | ||
|
|
d7409bec49 | ||
|
|
17b1f599ff | ||
|
|
81c85d712e | ||
|
|
2eae8bbb64 | ||
|
|
2a61f51e06 | ||
|
|
3e3c3e3bb4 | ||
|
|
7b41a89b8e | ||
|
|
36429da6da | ||
|
|
30f6ab5976 | ||
|
|
99a46f2a85 | ||
|
|
6754b1f2e1 | ||
|
|
122ad2b771 | ||
|
|
714e8ade1a | ||
|
|
4a6ea38be0 | ||
|
|
8bc7f0b71f | ||
|
|
9a912c16ad | ||
|
|
10a84a1356 | ||
|
|
901201a7af | ||
|
|
a57a1dfc47 | ||
|
|
577780cb90 | ||
|
|
b45eef9140 | ||
|
|
907853bbba | ||
|
|
17f3ffd00c | ||
|
|
8b86e17ac8 | ||
|
|
d6b6832a1d | ||
|
|
9dfc1cdc4c | ||
|
|
673c39d798 | ||
|
|
8ca84d183c | ||
|
|
9577aa2abe | ||
|
|
227be184ac | ||
|
|
d677f65eeb | ||
|
|
92f86bcca2 | ||
|
|
2015068d9e | ||
|
|
8a280c2f9d | ||
|
|
34fd6f9502 | ||
|
|
02acb36e47 | ||
|
|
280c0e0348 | ||
|
|
8528f0d848 | ||
|
|
917e881faa | ||
|
|
96b1efcfdc | ||
|
|
794e03b2cb | ||
|
|
a285c46b97 | ||
|
|
4da6294ef2 | ||
|
|
16ed9a7e8e | ||
|
|
798efbc22f | ||
|
|
60c789b6c7 | ||
|
|
f83d95136d | ||
|
|
f96ed0af26 | ||
|
|
1539664026 | ||
|
|
8aff775d0e | ||
|
|
94248eafe9 | ||
|
|
02335bba3f | ||
|
|
dad9578b83 | ||
|
|
c043788b09 | ||
|
|
e5bc416b46 |
@@ -9,9 +9,9 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.5",
|
||||
"@redocly/cli": "2.14.9",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.5.1",
|
||||
"@preact/signals": "2.6.1",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@zumer/snapdom": "2.0.1",
|
||||
"@zumer/snapdom": "2.0.2",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
@@ -44,7 +44,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -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",
|
||||
"lightningcss": "1.30.2",
|
||||
"happy-dom": "20.3.7",
|
||||
"lightningcss": "1.31.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
"vite-plugin-static-copy": "3.1.5"
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("removes boolean normally without inheritance", async () => {
|
||||
@@ -91,7 +91,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "false",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("overrides boolean with inherited false", async () => {
|
||||
@@ -112,7 +112,7 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
it("deletes override boolean with inherited false with already existing value", async () => {
|
||||
@@ -134,6 +134,6 @@ describe("Set boolean with inheritance", () => {
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,13 +14,13 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
|
||||
});
|
||||
}
|
||||
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false, componentId?: string) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
isInheritable,
|
||||
}, componentId);
|
||||
}
|
||||
|
||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
@@ -117,15 +117,15 @@ function removeOwnedRelationByName(note: FNote, relationName: string) {
|
||||
* @param name the name of the attribute to set.
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined, componentId?: string) {
|
||||
if (value !== null && value !== undefined) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }, componentId);
|
||||
} else {
|
||||
// Remove the attribute if it exists on the server but we don't define a value for it.
|
||||
const attributeId = note.getAttribute(type, name)?.attributeId;
|
||||
if (attributeId) {
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
|
||||
* @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
|
||||
*/
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true, componentId?: string) {
|
||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||
|
||||
if (branchIdsToDelete.length === 0) {
|
||||
@@ -139,9 +139,9 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
||||
const branch = froca.getBranch(branchIdToDelete);
|
||||
|
||||
if (deleteAllClones && branch) {
|
||||
await server.remove(`notes/${branch.noteId}${query}`);
|
||||
await server.remove(`notes/${branch.noteId}${query}`, componentId);
|
||||
} else {
|
||||
await server.remove(`branches/${branchIdToDelete}${query}`);
|
||||
await server.remove(`branches/${branchIdToDelete}${query}`, componentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import shortcuts, { isIMEComposing,keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -62,9 +62,10 @@ describe("shortcuts", () => {
|
||||
});
|
||||
|
||||
describe("keyMatches", () => {
|
||||
const createKeyboardEvent = (key: string, code?: string) => ({
|
||||
const createKeyboardEvent = (key: string, code?: string, extraProps: Partial<KeyboardEvent> = {}) => ({
|
||||
key,
|
||||
code: code || `Key${key.toUpperCase()}`
|
||||
code: code || `Key${key.toUpperCase()}`,
|
||||
...extraProps
|
||||
} as KeyboardEvent);
|
||||
|
||||
it("should match regular letter keys using key code", () => {
|
||||
@@ -102,17 +103,23 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match azerty keys", () => {
|
||||
const event = createKeyboardEvent("A", "KeyQ");
|
||||
expect(keyMatches(event, "a")).toBe(true);
|
||||
expect(keyMatches(event, "q")).toBe(false);
|
||||
});
|
||||
|
||||
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA", { altKey: true });
|
||||
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
|
||||
|
||||
// Option + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH", { altKey: true });
|
||||
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
|
||||
|
||||
// Option + S produces 'ß'
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS", { altKey: true });
|
||||
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -216,6 +223,15 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("matches azerty", () => {
|
||||
const event = createKeyboardEvent({
|
||||
key: "a",
|
||||
code: "KeyQ",
|
||||
ctrlKey: true
|
||||
});
|
||||
expect(matchesShortcut(event, "Ctrl+A")).toBe(true);
|
||||
});
|
||||
|
||||
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent({
|
||||
@@ -249,7 +265,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should not bind shortcuts when handler is null", () => {
|
||||
@@ -280,7 +296,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should fall back to document when element is empty", () => {
|
||||
@@ -290,7 +306,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,7 +317,7 @@ describe("shortcuts", () => {
|
||||
|
||||
shortcuts.removeGlobalShortcut("test-namespace");
|
||||
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -110,9 +110,8 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
||||
}
|
||||
};
|
||||
|
||||
// Add the event listener in capture phase to intercept events before they reach
|
||||
// child elements like CodeMirror
|
||||
element.addEventListener('keydown', listener, true);
|
||||
// Add the event listener
|
||||
element.addEventListener('keydown', listener);
|
||||
|
||||
// Store the binding for later cleanup
|
||||
const binding: ShortcutBinding = {
|
||||
@@ -139,16 +138,15 @@ export function removeIndividualBinding(binding: ShortcutBinding) {
|
||||
if (activeBindingsInNamespace) {
|
||||
activeBindings.set(key, activeBindingsInNamespace.filter(aBinding => aBinding.handler === binding.handler));
|
||||
}
|
||||
// Remove listener with capture phase to match how it was added
|
||||
binding.element.removeEventListener("keydown", binding.listener, true);
|
||||
binding.element.removeEventListener("keydown", binding.listener);
|
||||
}
|
||||
|
||||
function removeNamespaceBindings(namespace: string) {
|
||||
const bindings = activeBindings.get(namespace);
|
||||
if (bindings) {
|
||||
// Remove all event listeners for this namespace
|
||||
bindings.forEach(binding => {
|
||||
// Remove listener with capture phase to match how it was added
|
||||
binding.element.removeEventListener('keydown', binding.listener, true);
|
||||
binding.element.removeEventListener('keydown', binding.listener);
|
||||
});
|
||||
activeBindings.delete(namespace);
|
||||
}
|
||||
@@ -217,9 +215,12 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
// For letter keys, use the physical key code for consistency
|
||||
// On macOS, Option/Alt key produces special characters, so we must use e.code
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
if (e.altKey) {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
import { type AutoHighlightResult, ensureMimeTypes, highlight, highlightAuto, type HighlightResult, loadTheme, type Theme,Themes } from "@triliumnext/highlightjs";
|
||||
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { t } from "./i18n.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
import options from "./options.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { isShare } from "./utils.js";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
|
||||
let highlightingLoaded = false;
|
||||
|
||||
@@ -76,13 +77,15 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
||||
}
|
||||
|
||||
export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
if (highlightingLoaded) {
|
||||
if (!mimeTypeHint && highlightingLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load theme.
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
if (!highlightingLoaded) {
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
}
|
||||
|
||||
// Load mime types.
|
||||
let mimeTypes: MimeType[];
|
||||
@@ -94,7 +97,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
enabled: true,
|
||||
mime: mimeTypeHint.replace("-", "/")
|
||||
}
|
||||
]
|
||||
];
|
||||
} else {
|
||||
mimeTypes = mime_types.getMimeTypes();
|
||||
}
|
||||
@@ -124,9 +127,9 @@ export function isSyntaxHighlightEnabled() {
|
||||
if (!isShare) {
|
||||
const theme = options.get("codeBlockTheme");
|
||||
return !!theme && theme !== "none";
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -217,6 +217,7 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
|
||||
id={inputId}
|
||||
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
|
||||
value={valueAttr.value}
|
||||
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
|
||||
placeholder={t("promoted_attributes.unset-field-placeholder")}
|
||||
data-attribute-id={valueAttr.attributeId}
|
||||
data-attribute-type={valueAttr.type}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CreateChildrenResponse } from "@triliumnext/commons";
|
||||
import server from "../../../services/server";
|
||||
import { AttributeRow, CreateChildrenResponse } from "@triliumnext/commons";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { setAttribute, setLabel } from "../../../services/attributes";
|
||||
import froca from "../../../services/froca";
|
||||
import server from "../../../services/server";
|
||||
|
||||
interface NewEventOpts {
|
||||
title: string;
|
||||
@@ -10,6 +10,7 @@ interface NewEventOpts {
|
||||
endDate?: string | null;
|
||||
startTime?: string | null;
|
||||
endTime?: string | null;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
interface ChangeEventOpts {
|
||||
@@ -17,30 +18,48 @@ interface ChangeEventOpts {
|
||||
endDate?: string | null;
|
||||
startTime?: string | null;
|
||||
endTime?: string | null;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
export async function newEvent(parentNote: FNote, { title, startDate, endDate, startTime, endTime }: NewEventOpts) {
|
||||
// Create the note.
|
||||
const { note } = await server.post<CreateChildrenResponse>(`notes/${parentNote.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
export async function newEvent(parentNote: FNote, { title, startDate, endDate, startTime, endTime, componentId }: NewEventOpts) {
|
||||
const attributes: Omit<AttributeRow, "noteId" | "attributeId">[] = [];
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "startDate",
|
||||
value: startDate
|
||||
});
|
||||
|
||||
// Set the attributes.
|
||||
setLabel(note.noteId, "startDate", startDate);
|
||||
if (endDate) {
|
||||
setLabel(note.noteId, "endDate", endDate);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "endDate",
|
||||
value: endDate
|
||||
});
|
||||
}
|
||||
if (startTime) {
|
||||
setLabel(note.noteId, "startTime", startTime);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "startTime",
|
||||
value: startTime
|
||||
});
|
||||
}
|
||||
if (endTime) {
|
||||
setLabel(note.noteId, "endTime", endTime);
|
||||
attributes.push({
|
||||
type: "label",
|
||||
name: "endTime",
|
||||
value: endTime
|
||||
});
|
||||
}
|
||||
|
||||
// Create the note.
|
||||
await server.post<CreateChildrenResponse>(`notes/${parentNote.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text",
|
||||
attributes
|
||||
}, componentId);
|
||||
}
|
||||
|
||||
export async function changeEvent(note: FNote, { startDate, endDate, startTime, endTime }: ChangeEventOpts) {
|
||||
export async function changeEvent(note: FNote, { startDate, endDate, startTime, endTime, componentId }: ChangeEventOpts) {
|
||||
// Don't store the end date if it's empty.
|
||||
if (endDate === startDate) {
|
||||
endDate = undefined;
|
||||
@@ -52,12 +71,12 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
|
||||
let endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endDate").shift()?.value||"endDate";
|
||||
|
||||
const noteId = note.noteId;
|
||||
setLabel(noteId, startAttribute, startDate);
|
||||
setAttribute(note, "label", endAttribute, endDate);
|
||||
setLabel(noteId, startAttribute, startDate, false, componentId);
|
||||
setAttribute(note, "label", endAttribute, endDate, componentId);
|
||||
|
||||
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
|
||||
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
|
||||
|
||||
setAttribute(note, "label", startAttribute, startTime);
|
||||
setAttribute(note, "label", endAttribute, endTime);
|
||||
setAttribute(note, "label", startAttribute, startTime, componentId);
|
||||
setAttribute(note, "label", endAttribute, endTime, componentId);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import branches from "../../../services/branches";
|
||||
import { getArchiveMenuItem } from "../../../menus/context_menu_utils";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote) {
|
||||
export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parentNote: FNote, componentId?: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -30,16 +30,16 @@ export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parent
|
||||
}
|
||||
|
||||
if (branchIdToDelete) {
|
||||
await branches.deleteNotes([ branchIdToDelete ], false, false);
|
||||
await branches.deleteNotes([ branchIdToDelete ], false, false, componentId);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
kind: "custom",
|
||||
componentFn: () => NoteColorPicker({note: note})
|
||||
componentFn: () => NoteColorPicker({note})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, note.noteId),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
|
||||
import froca from "../../../services/froca";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import server from "../../../services/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import froca from "../../../services/froca";
|
||||
import server from "../../../services/server";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
|
||||
interface Event {
|
||||
startDate: string,
|
||||
endDate?: string | null,
|
||||
@@ -105,7 +106,8 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
|
||||
endDate = (endTime ? `${endDate}T${endTime}:00` : endDate);
|
||||
const eventData: EventInput = {
|
||||
title: title,
|
||||
id: note.noteId,
|
||||
title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}?popup`,
|
||||
noteId: note.noteId,
|
||||
@@ -148,12 +150,12 @@ async function parseCustomTitle(customTitlettributeName: string | null, note: FN
|
||||
}
|
||||
|
||||
async function buildDisplayedAttributes(note: FNote, calendarDisplayedAttributes: string[]) {
|
||||
const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name))
|
||||
const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name));
|
||||
const result: Array<[string, string]> = [];
|
||||
|
||||
for (const attribute of filteredDisplayedAttributes) {
|
||||
if (attribute.type === "label") result.push([attribute.name, attribute.value]);
|
||||
else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""])
|
||||
else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""]);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, Local
|
||||
import { DateClickArg } from "@fullcalendar/interaction";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
import { RefObject } from "preact";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
@@ -17,6 +17,7 @@ import { isMobile } from "../../../services/utils";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { changeEvent, newEvent } from "./api";
|
||||
@@ -87,6 +88,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
};
|
||||
|
||||
export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarViewData>) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = useRef<FullCalendar>(null);
|
||||
|
||||
@@ -105,26 +107,34 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const eventBuilder = useMemo(() => {
|
||||
if (!isCalendarRoot) {
|
||||
return async () => await buildEvents(noteIds);
|
||||
}
|
||||
}
|
||||
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
|
||||
|
||||
}, [isCalendarRoot, noteIds]);
|
||||
|
||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||
const locale = useLocale();
|
||||
|
||||
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||
const { eventDidMount } = useEventDisplayCustomization(note, parentComponent?.componentId);
|
||||
const editingProps = useEditing(note, isEditable, isCalendarRoot, parentComponent?.componentId);
|
||||
|
||||
// React to changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) // note title change.
|
||||
|| loadResults.getAttributeRows().some((a) => noteIds.includes(a.noteId ?? ""))) // subnote change.
|
||||
{
|
||||
const api = calendarRef.current;
|
||||
if (!api) return;
|
||||
|
||||
// Subnote attribute change.
|
||||
if (loadResults.getAttributeRows(parentComponent?.componentId).some((a) => noteIds.includes(a.noteId ?? ""))) {
|
||||
// Defer execution after the load results are processed so that the event builder has the updated data to work with.
|
||||
setTimeout(() => {
|
||||
calendarRef.current?.refetchEvents();
|
||||
}, 0);
|
||||
setTimeout(() => api.refetchEvents(), 0);
|
||||
return; // early return since we'll refresh the events anyway
|
||||
}
|
||||
|
||||
// Title change.
|
||||
for (const noteId of loadResults.getNoteIds().filter(noteId => noteIds.includes(noteId))) {
|
||||
const event = api.getEventById(noteId);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
if (!event || !note) continue;
|
||||
event.setProp("title", note.title);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,7 +232,7 @@ function useLocale() {
|
||||
return calendarLocale;
|
||||
}
|
||||
|
||||
function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean, componentId: string | undefined) {
|
||||
const onCalendarSelection = useCallback(async (e: DateSelectArg) => {
|
||||
const { startDate, endDate } = parseStartEndDateFromEvent(e);
|
||||
if (!startDate) return;
|
||||
@@ -234,8 +244,8 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
return;
|
||||
}
|
||||
|
||||
newEvent(note, { title, startDate, endDate, startTime, endTime });
|
||||
}, [ note ]);
|
||||
newEvent(note, { title, startDate, endDate, startTime, endTime, componentId });
|
||||
}, [ note, componentId ]);
|
||||
|
||||
const onEventChange = useCallback(async (e: EventChangeArg) => {
|
||||
const { startDate, endDate } = parseStartEndDateFromEvent(e.event);
|
||||
@@ -244,8 +254,8 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
const { startTime, endTime } = parseStartEndTimeFromEvent(e.event);
|
||||
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||
if (!note) return;
|
||||
changeEvent(note, { startDate, endDate, startTime, endTime });
|
||||
}, []);
|
||||
changeEvent(note, { startDate, endDate, startTime, endTime, componentId });
|
||||
}, [ componentId ]);
|
||||
|
||||
// Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root.
|
||||
const onDateClick = useCallback(async (e: DateClickArg) => {
|
||||
@@ -264,7 +274,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function useEventDisplayCustomization(parentNote: FNote) {
|
||||
function useEventDisplayCustomization(parentNote: FNote, componentId: string | undefined) {
|
||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||
|
||||
@@ -321,7 +331,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
|
||||
const note = await froca.getNote(e.event.extendedProps.noteId);
|
||||
if (!note) return;
|
||||
|
||||
openCalendarContextMenu(contextMenuEvent, note, parentNote);
|
||||
openCalendarContextMenu(contextMenuEvent, note, parentNote, componentId);
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
|
||||
@@ -82,6 +82,10 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail.full-height {
|
||||
flex-grow: 0;
|
||||
height: 100%;
|
||||
@@ -106,4 +110,4 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from "clsx";
|
||||
import server from "../../services/server";
|
||||
import { TargetedMouseEvent, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Dayjs, getWeekInfo, WeekSettings } from "@triliumnext/commons";
|
||||
import { Dayjs } from "@triliumnext/commons";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
interface DateNotesForMonth {
|
||||
@@ -22,7 +22,6 @@ const DAYS_OF_WEEK = [
|
||||
|
||||
interface DateRangeInfo {
|
||||
weekNumbers: number[];
|
||||
weekYears: number[];
|
||||
dates: Dayjs[];
|
||||
}
|
||||
|
||||
@@ -37,27 +36,19 @@ export interface CalendarArgs {
|
||||
|
||||
export default function Calendar(args: CalendarArgs) {
|
||||
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
|
||||
const [ firstWeekOfYear ] = useTriliumOptionInt("firstWeekOfYear");
|
||||
const [ minDaysInFirstWeek ] = useTriliumOptionInt("minDaysInFirstWeek");
|
||||
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
|
||||
|
||||
const weekSettings = {
|
||||
firstDayOfWeek: firstDayOfWeekISO,
|
||||
firstWeekOfYear: firstWeekOfYear ?? 0,
|
||||
minDaysInFirstWeek: minDaysInFirstWeek ?? 4
|
||||
};
|
||||
|
||||
const date = args.date;
|
||||
const firstDay = date.startOf('month');
|
||||
const firstDayISO = firstDay.isoWeekday();
|
||||
const monthInfo = getMonthInformation(date, firstDayISO, weekSettings);
|
||||
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
|
||||
<div className="calendar-body" data-calendar-area="month">
|
||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} weekSettings={weekSettings} {...args} />}
|
||||
<CurrentMonthDays weekSettings={weekSettings} {...args} />
|
||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} {...args} />}
|
||||
<CurrentMonthDays firstDayOfWeekISO={firstDayOfWeekISO} {...args} />
|
||||
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
|
||||
</div>
|
||||
</>
|
||||
@@ -76,7 +67,7 @@ function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }
|
||||
)
|
||||
}
|
||||
|
||||
function PreviousMonthDays({ date, info: { dates, weekNumbers, weekYears }, weekSettings, ...args }: { date: Dayjs, info: DateRangeInfo, weekSettings: WeekSettings } & CalendarArgs) {
|
||||
function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) {
|
||||
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
|
||||
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
|
||||
|
||||
@@ -86,28 +77,27 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers, weekYears }, week
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeek date={date} weekNumber={weekNumbers[0]} weekYear={weekYears[0]} {...args} />
|
||||
<CalendarWeek date={date} weekNumber={weekNumbers[0]} {...args} />
|
||||
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrentMonthDays({ date, weekSettings, ...args }: { date: Dayjs, weekSettings: WeekSettings } & CalendarArgs) {
|
||||
function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
|
||||
let dateCursor = date;
|
||||
const currentMonth = date.month();
|
||||
const items: VNode[] = [];
|
||||
const curMonthString = date.format('YYYY-MM');
|
||||
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
|
||||
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = weekSettings;
|
||||
|
||||
useEffect(() => {
|
||||
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
|
||||
}, [ date ]);
|
||||
|
||||
while (dateCursor.month() === currentMonth) {
|
||||
const { weekYear, weekNumber } = getWeekInfo(dateCursor, weekSettings);
|
||||
if (dateCursor.isoWeekday() === firstDayOfWeek) {
|
||||
items.push(<CalendarWeek key={`${weekYear}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} weekYear={weekYear} {...args}/>)
|
||||
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
|
||||
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
|
||||
items.push(<CalendarWeek key={`${dateCursor.year()}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} {...args}/>)
|
||||
}
|
||||
|
||||
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
|
||||
@@ -151,8 +141,14 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }: { weekNumber: number, weekYear: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
||||
const weekString = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
|
||||
function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
||||
const localDate = date.local();
|
||||
|
||||
// Handle case where week is in between years.
|
||||
let year = localDate.year();
|
||||
if (localDate.month() === 11 && weekNumber === 1) year++;
|
||||
|
||||
const weekString = `${year}-W${String(weekNumber).padStart(2, '0')}`;
|
||||
|
||||
if (onWeekClicked) {
|
||||
return (
|
||||
@@ -173,33 +169,33 @@ function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }:
|
||||
>{weekNumber}</span>);
|
||||
}
|
||||
|
||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings) {
|
||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
|
||||
return {
|
||||
prevMonth: getPrevMonthDays(date, firstDayISO, weekSettings),
|
||||
nextMonth: getNextMonthDays(date, weekSettings.firstDayOfWeek)
|
||||
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
|
||||
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
|
||||
}
|
||||
}
|
||||
|
||||
function getPrevMonthDays(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings): DateRangeInfo {
|
||||
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
|
||||
const daysToAdd = (firstDayISO - weekSettings.firstDayOfWeek + 7) % 7;
|
||||
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
const firstDay = date.startOf('month');
|
||||
const { weekYear, weekNumber } = getWeekInfo(firstDay, weekSettings);
|
||||
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
|
||||
|
||||
// Get dates from previous month
|
||||
for (let i = daysToAdd - 1; i >= 0; i--) {
|
||||
dates.push(prevMonthLastDay.subtract(i, 'day'));
|
||||
}
|
||||
|
||||
return { weekNumbers: [ weekNumber ], weekYears: [ weekYear ], dates };
|
||||
return { weekNumbers: [ weekNumber ], dates };
|
||||
}
|
||||
|
||||
function getNextMonthDays(date: Dayjs, firstDayOfWeek: number): DateRangeInfo {
|
||||
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
const lastDayOfMonth = date.endOf('month');
|
||||
const lastDayISO = lastDayOfMonth.isoWeekday();
|
||||
const lastDayOfUserWeek = ((firstDayOfWeek + 6 - 1) % 7) + 1;
|
||||
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
|
||||
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
@@ -210,5 +206,16 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeek: number): DateRangeInfo {
|
||||
dates.push(nextMonthFirstDay.add(i, 'day'));
|
||||
}
|
||||
}
|
||||
return { weekNumbers: [], weekYears: [], dates };
|
||||
return { weekNumbers: [], dates };
|
||||
}
|
||||
|
||||
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
|
||||
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
|
||||
return weekStart.isoWeek();
|
||||
}
|
||||
|
||||
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
|
||||
const currentISO = date.isoWeekday();
|
||||
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import render from "../../services/render";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import Alert from "../react/Alert";
|
||||
import "./Render.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import render from "../../services/render";
|
||||
import Alert from "../react/Alert";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -31,6 +34,13 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
refresh();
|
||||
});
|
||||
|
||||
// Refresh on attribute change.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(a => a.type === "relation" && a.name === "renderNote" && attributes.isAffecting(a, note))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Integration with search.
|
||||
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
|
||||
@@ -20,6 +20,9 @@ export interface CanvasContent {
|
||||
appState: Partial<AppState>;
|
||||
}
|
||||
|
||||
/** Subset of the app state that should be persisted whenever they change. This explicitly excludes transient state like the current selection or zoom level. */
|
||||
type ImportantAppState = Pick<AppState, "gridModeEnabled" | "viewBackgroundColor">;
|
||||
|
||||
export default function useCanvasPersistence(note: FNote, noteContext: NoteContext | null | undefined, apiRef: RefObject<ExcalidrawImperativeAPI>, theme: AppState["theme"], isReadOnly: boolean): Partial<ExcalidrawProps> {
|
||||
const libraryChanged = useRef(false);
|
||||
|
||||
@@ -37,6 +40,8 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
const libraryCache = useRef<LibraryItem[]>([]);
|
||||
const attachmentMetadata = useRef<AttachmentMetadata[]>([]);
|
||||
|
||||
const appStateToCompare = useRef<Partial<ImportantAppState>>({});
|
||||
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteContext,
|
||||
@@ -47,7 +52,6 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
|
||||
libraryCache.current = [];
|
||||
attachmentMetadata.current = [];
|
||||
currentSceneVersion.current = -1;
|
||||
|
||||
// load saved content into excalidraw canvas
|
||||
let content: CanvasContent = {
|
||||
@@ -65,6 +69,9 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
|
||||
loadData(api, content, theme);
|
||||
|
||||
// Initialize tracking state after loading to prevent redundant updates from initial onChange events
|
||||
currentSceneVersion.current = getSceneVersion(api.getSceneElements());
|
||||
|
||||
// load the library state
|
||||
loadLibrary(note).then(({ libraryItems, metadata }) => {
|
||||
// Update the library and save to independent variables
|
||||
@@ -78,7 +85,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
async getData() {
|
||||
const api = apiRef.current;
|
||||
if (!api) return;
|
||||
const { content, svg } = await getData(api);
|
||||
const { content, svg } = await getData(api, appStateToCompare);
|
||||
const attachments: SavedData["attachments"] = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
|
||||
|
||||
// libraryChanged is unset in dataSaved()
|
||||
@@ -149,21 +156,47 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
const oldSceneVersion = currentSceneVersion.current;
|
||||
const newSceneVersion = getSceneVersion(apiRef.current.getSceneElements());
|
||||
|
||||
if (newSceneVersion !== oldSceneVersion) {
|
||||
let hasChanges = (newSceneVersion !== oldSceneVersion);
|
||||
|
||||
// There are cases where the scene version does not change, but appState did.
|
||||
if (!hasChanges) {
|
||||
const importantAppState = appStateToCompare.current;
|
||||
const currentAppState = apiRef.current.getAppState();
|
||||
for (const key in importantAppState) {
|
||||
if (importantAppState[key as keyof ImportantAppState] !== currentAppState[key as keyof ImportantAppState]) {
|
||||
hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
currentSceneVersion.current = newSceneVersion;
|
||||
}
|
||||
},
|
||||
onLibraryChange: () => {
|
||||
libraryChanged.current = true;
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
onLibraryChange: (libraryItems) => {
|
||||
if (!apiRef.current || isReadOnly) return;
|
||||
|
||||
// Check if library actually changed by comparing with cached state
|
||||
const hasChanges =
|
||||
libraryItems.length !== libraryCache.current.length ||
|
||||
libraryItems.some(item => {
|
||||
const cachedItem = libraryCache.current.find(cached => cached.id === item.id);
|
||||
return !cachedItem || cachedItem.name !== item.name;
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
libraryChanged.current = true;
|
||||
spacedUpdate.resetUpdateTimer();
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getData(api: ExcalidrawImperativeAPI) {
|
||||
async function getData(api: ExcalidrawImperativeAPI, appStateToCompare: RefObject<Partial<ImportantAppState>>) {
|
||||
const elements = api.getSceneElements();
|
||||
const appState = api.getAppState();
|
||||
|
||||
@@ -188,6 +221,12 @@ async function getData(api: ExcalidrawImperativeAPI) {
|
||||
}
|
||||
});
|
||||
|
||||
const importantAppState: ImportantAppState = {
|
||||
gridModeEnabled: appState.gridModeEnabled,
|
||||
viewBackgroundColor: appState.viewBackgroundColor
|
||||
};
|
||||
appStateToCompare.current = importantAppState;
|
||||
|
||||
const content = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -197,7 +236,7 @@ async function getData(api: ExcalidrawImperativeAPI) {
|
||||
scrollX: appState.scrollX,
|
||||
scrollY: appState.scrollY,
|
||||
zoom: appState.zoom,
|
||||
gridModeEnabled: appState.gridModeEnabled
|
||||
...importantAppState
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { useNoteBlob, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
@@ -30,12 +30,22 @@ 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);
|
||||
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export default function SplitEditor(props: SplitEditorProps) {
|
||||
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
|
||||
|
||||
const editor = (!readOnly &&
|
||||
if (readOnly) {
|
||||
return <ReadOnlyView {...props} />;
|
||||
}
|
||||
|
||||
return <EditorWithSplit {...props} />;
|
||||
|
||||
}
|
||||
|
||||
function EditorWithSplit({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
|
||||
const editor = (
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
@@ -53,19 +63,14 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
</div>
|
||||
);
|
||||
|
||||
const preview = (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const preview = <PreviewContainer
|
||||
error={error}
|
||||
previewContent={previewContent}
|
||||
previewButtons={previewButtons}
|
||||
/>;
|
||||
|
||||
useEffect(() => {
|
||||
if (!utils.isDesktop() || !containerRef.current || readOnly) return;
|
||||
if (!utils.isDesktop() || !containerRef.current) return;
|
||||
const elements = Array.from(containerRef.current?.children) as HTMLElement[];
|
||||
const splitInstance = Split(elements, {
|
||||
rtl: glob.isRtl,
|
||||
@@ -76,10 +81,10 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
});
|
||||
|
||||
return () => splitInstance.destroy();
|
||||
}, [ readOnly, splitEditorOrientation ]);
|
||||
}, [ splitEditorOrientation ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
@@ -87,6 +92,43 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyView({ ...props }: SplitEditorProps) {
|
||||
const { note, onContentChanged } = props;
|
||||
const content = useNoteBlob(note);
|
||||
const onContentChangedRef = useRef(onContentChanged);
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current = onContentChanged;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current?.(content?.content ?? "");
|
||||
}, [ content ]);
|
||||
|
||||
return (
|
||||
<div className={`note-detail-split note-detail-printable ${props.className} split-read-only`}>
|
||||
<PreviewContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewContainer({ error, previewContent, previewButtons }: {
|
||||
error?: string | null;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
}) {
|
||||
return (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
return <ActionButton
|
||||
{...props}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import server from "../../../services/server";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
import { RefObject } from "preact";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import utils from "../../../services/utils";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import utils from "../../../services/utils";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
|
||||
interface SvgSplitEditorProps extends Omit<SplitEditorProps, "previewContent"> {
|
||||
/**
|
||||
@@ -144,7 +145,7 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg: string | undefined) {
|
||||
@@ -181,7 +182,7 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
|
||||
lastPanZoom.current = {
|
||||
pan: zoomInstance.getPan(),
|
||||
zoom: zoomInstance.getZoom()
|
||||
}
|
||||
};
|
||||
zoomRef.current = undefined;
|
||||
zoomInstance.destroy();
|
||||
};
|
||||
|
||||
@@ -191,7 +191,6 @@ function ExperimentalOptions() {
|
||||
values={filteredExperimentalFeatures}
|
||||
keyProperty="id"
|
||||
titleProperty="name"
|
||||
descriptionProperty="description"
|
||||
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
|
||||
/>
|
||||
</OptionsSection>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import FormCheckbox from "../../../react/FormCheckbox";
|
||||
|
||||
interface CheckboxListProps<T> {
|
||||
values: T[];
|
||||
keyProperty: keyof T;
|
||||
titleProperty?: keyof T;
|
||||
disabledProperty?: keyof T;
|
||||
descriptionProperty?: keyof T;
|
||||
currentValue: string[];
|
||||
onChange: (newValues: string[]) => void;
|
||||
columnWidth?: string;
|
||||
}
|
||||
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
function toggleValue(value: string) {
|
||||
if (currentValue.includes(value)) {
|
||||
// Already there, needs removing.
|
||||
@@ -25,17 +22,20 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
|
||||
return (
|
||||
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
||||
{values.map(value => (
|
||||
<li key={String(value[keyProperty])}>
|
||||
<FormCheckbox
|
||||
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
name={String(value[keyProperty])}
|
||||
currentValue={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
|
||||
onChange={() => toggleValue(String(value[keyProperty]))}
|
||||
/>
|
||||
<li>
|
||||
<label className="tn-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={String(value[keyProperty])}
|
||||
checked={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,15 @@ export default defineConfig(() => ({
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
entryFileNames: (chunk) => {
|
||||
// We enforce a hash in the main index file to avoid caching issues, this only works because we have the HTML entry point.
|
||||
if (chunk.name === "index") {
|
||||
return "src/[name]-[hash].js";
|
||||
}
|
||||
|
||||
// For EJS-rendered pages (e.g. login) we need to have a stable name.
|
||||
return "src/[name].js";
|
||||
},
|
||||
chunkFileNames: "src/[name]-[hash].js",
|
||||
assetFileNames: "src/[name]-[hash].[ext]",
|
||||
manualChunks: {
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"bindings": "1.5.0",
|
||||
"bootstrap": "5.3.8",
|
||||
"chardet": "2.1.1",
|
||||
"cheerio": "1.1.2",
|
||||
"cheerio": "1.2.0",
|
||||
"chokidar": "5.0.0",
|
||||
"cls-hooked": "4.2.2",
|
||||
"compression": "1.8.1",
|
||||
@@ -91,7 +91,7 @@
|
||||
"express-http-proxy": "2.1.2",
|
||||
"express-openid-connect": "2.19.4",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-session": "1.18.2",
|
||||
"express-session": "1.19.0",
|
||||
"file-uri-to-path": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"helmet": "8.1.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turnish": "1.7.1",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type BNote from "../becca/entities/bnote.js";
|
||||
|
||||
import attributeService from "./attributes.js";
|
||||
import cloningService from "./cloning.js";
|
||||
import { dayjs, Dayjs, getFirstDayOfWeek1, getWeekInfo, WeekSettings } from "@triliumnext/commons";
|
||||
import { dayjs, Dayjs } from "@triliumnext/commons";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import noteService from "./notes.js";
|
||||
import optionService from "./options.js";
|
||||
@@ -63,8 +63,7 @@ function getJournalNoteTitle(
|
||||
rootNote: BNote,
|
||||
timeUnit: TimeUnit,
|
||||
dateObj: Dayjs,
|
||||
number: number,
|
||||
weekYear?: number // Optional: the week year for cross-year weeks
|
||||
number: number
|
||||
) {
|
||||
const patterns = {
|
||||
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
|
||||
@@ -80,14 +79,9 @@ function getJournalNoteTitle(
|
||||
const numberStr = number.toString();
|
||||
const ordinalStr = ordinal(dateObj);
|
||||
|
||||
// For week notes, use the weekYear if provided (handles cross-year weeks)
|
||||
const yearForDisplay = (timeUnit === "week" && weekYear !== undefined)
|
||||
? weekYear.toString()
|
||||
: dateObj.format("YYYY");
|
||||
|
||||
const allReplacements: Record<string, string> = {
|
||||
// Common date formats
|
||||
"{year}": yearForDisplay,
|
||||
"{year}": dateObj.format("YYYY"),
|
||||
|
||||
// Month related
|
||||
"{isoMonth}": dateObj.format("YYYY-MM"),
|
||||
@@ -292,14 +286,6 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
return monthNote as unknown as BNote;
|
||||
}
|
||||
|
||||
function getWeekSettings(): WeekSettings {
|
||||
return {
|
||||
firstDayOfWeek: parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10),
|
||||
firstWeekOfYear: parseInt(optionService.getOptionOrNull("firstWeekOfYear") ?? "0", 10),
|
||||
minDaysInFirstWeek: parseInt(optionService.getOptionOrNull("minDaysInFirstWeek") ?? "4", 10)
|
||||
};
|
||||
}
|
||||
|
||||
function getWeekStartDate(date: Dayjs): Dayjs {
|
||||
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
|
||||
const day = date.isoWeekday();
|
||||
@@ -308,8 +294,9 @@ function getWeekStartDate(date: Dayjs): Dayjs {
|
||||
}
|
||||
|
||||
function getWeekNumberStr(date: Dayjs): string {
|
||||
const { weekYear, weekNumber } = getWeekInfo(date, getWeekSettings());
|
||||
return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
|
||||
const isoYear = date.isoWeekYear();
|
||||
const isoWeekNum = date.isoWeek();
|
||||
return `${isoYear}-W${isoWeekNum.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
|
||||
@@ -342,19 +329,17 @@ function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | n
|
||||
|
||||
const [ yearStr, weekNumStr ] = weekStr.trim().split("-W");
|
||||
const weekNumber = parseInt(weekNumStr);
|
||||
const weekYear = parseInt(yearStr);
|
||||
|
||||
// Calculate week start date based on user's first week of year settings.
|
||||
// This correctly handles cross-year weeks based on user preferences.
|
||||
const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, getWeekSettings());
|
||||
const startDate = firstDayOfWeek1.add(weekNumber - 1, "week");
|
||||
const endDate = startDate.add(6, "day");
|
||||
const firstDayOfYear = dayjs().year(parseInt(yearStr)).month(0).date(1);
|
||||
const weekStartDate = firstDayOfYear.add(weekNumber - 1, "week");
|
||||
const startDate = getWeekStartDate(weekStartDate);
|
||||
const endDate = dayjs(startDate).add(6, "day");
|
||||
|
||||
const startMonth = startDate.month();
|
||||
const endMonth = endDate.month();
|
||||
|
||||
const monthNote = getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
|
||||
const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber, weekYear);
|
||||
const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber);
|
||||
|
||||
sql.transactional(() => {
|
||||
weekNote = createNote(monthNote, noteTitle);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NoteType } from "@triliumnext/commons";
|
||||
import type { AttributeRow, NoteType } from "@triliumnext/commons";
|
||||
|
||||
export interface NoteParams {
|
||||
/** optionally can force specific noteId */
|
||||
@@ -24,4 +24,6 @@ export interface NoteParams {
|
||||
utcDateCreated?: string;
|
||||
ignoreForbiddenParents?: boolean;
|
||||
target?: "into";
|
||||
/** Attributes to be set on the note. These are set atomically on note creation, so entity changes are not sent for attributes defined here. */
|
||||
attributes?: Omit<AttributeRow, "noteId" | "attributeId">[];
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import sql from "./sql.js";
|
||||
import optionService from "./options.js";
|
||||
import { type AttachmentRow, type AttributeRow, type BranchRow, dayjs, type NoteRow } from "@triliumnext/commons";
|
||||
import fs from "fs";
|
||||
import html2plaintext from "html2plaintext";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
import url from "url";
|
||||
|
||||
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 BNote from "../becca/entities/bnote.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
import cls from "../services/cls.js";
|
||||
import log from "../services/log.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import { newEntityId, quoteRegex, toMap, unescapeHtml } from "../services/utils.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import entityChangesService from "./entity_changes.js";
|
||||
import eventService from "./events.js";
|
||||
import cls from "../services/cls.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import log from "../services/log.js";
|
||||
import { newEntityId, unescapeHtml, quoteRegex, toMap } from "../services/utils.js";
|
||||
import revisionService from "./revisions.js";
|
||||
import request from "./request.js";
|
||||
import path from "path";
|
||||
import url from "url";
|
||||
import becca from "../becca/becca.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import BAttribute from "../becca/entities/battribute.js";
|
||||
import BAttachment from "../becca/entities/battachment.js";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import htmlSanitizer from "./html_sanitizer.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
import noteTypesService from "./note_types.js";
|
||||
import fs from "fs";
|
||||
import ws from "./ws.js";
|
||||
import html2plaintext from "html2plaintext";
|
||||
import type { AttachmentRow, AttributeRow, BranchRow, NoteRow } from "@triliumnext/commons";
|
||||
import type TaskContext from "./task_context.js";
|
||||
import type { NoteParams } from "./note-interface.js";
|
||||
import imageService from "./image.js";
|
||||
import { t } from "i18next";
|
||||
import noteTypesService from "./note_types.js";
|
||||
import type { NoteParams } from "./note-interface.js";
|
||||
import optionService from "./options.js";
|
||||
import request from "./request.js";
|
||||
import revisionService from "./revisions.js";
|
||||
import sql from "./sql.js";
|
||||
import type TaskContext from "./task_context.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
interface FoundLink {
|
||||
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink";
|
||||
@@ -47,14 +47,13 @@ function getNewNotePosition(parentNote: BNote) {
|
||||
.reduce((min, note) => Math.min(min, note?.notePosition || 0), 0);
|
||||
|
||||
return minNotePos - 10;
|
||||
} else {
|
||||
const maxNotePos = parentNote
|
||||
.getChildBranches()
|
||||
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
|
||||
.reduce((max, note) => Math.max(max, note?.notePosition || 0), 0);
|
||||
|
||||
return maxNotePos + 10;
|
||||
}
|
||||
const maxNotePos = parentNote
|
||||
.getChildBranches()
|
||||
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
|
||||
.reduce((max, note) => Math.max(max, note?.notePosition || 0), 0);
|
||||
|
||||
return maxNotePos + 10;
|
||||
}
|
||||
|
||||
function triggerNoteTitleChanged(note: BNote) {
|
||||
@@ -88,7 +87,7 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) {
|
||||
new BAttribute({
|
||||
noteId: childNote.noteId,
|
||||
type: attr.type,
|
||||
name: name,
|
||||
name,
|
||||
value: attr.value,
|
||||
position: attr.position,
|
||||
isInheritable: attr.isInheritable
|
||||
@@ -222,6 +221,14 @@ function createNewNote(params: NoteParams): {
|
||||
utcDateCreated: params.utcDateCreated
|
||||
}).save();
|
||||
|
||||
// Create attributes atomically.
|
||||
for (const attribute of params.attributes || []) {
|
||||
new BAttribute({
|
||||
...attribute,
|
||||
noteId: note.noteId
|
||||
}).save();
|
||||
}
|
||||
|
||||
note.setContent(params.content);
|
||||
|
||||
branch = new BBranch({
|
||||
@@ -260,7 +267,7 @@ function createNewNote(params: NoteParams): {
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "blobs", entity: note });
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: "branches", entity: branch });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "branches", entity: branch });
|
||||
eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote });
|
||||
eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote });
|
||||
|
||||
log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
|
||||
|
||||
@@ -308,9 +315,8 @@ function createNewNoteWithTarget(target: "into" | "after" | "before", targetBran
|
||||
entityChangesService.putNoteReorderingEntityChange(params.parentNoteId);
|
||||
|
||||
return retObject;
|
||||
} else {
|
||||
throw new Error(`Unknown target '${target}'`);
|
||||
}
|
||||
throw new Error(`Unknown target '${target}'`);
|
||||
}
|
||||
|
||||
function protectNoteRecursively(note: BNote, protect: boolean, includingSubTree: boolean, taskContext: TaskContext<"protectNotes">) {
|
||||
@@ -488,7 +494,7 @@ function findRelationMapLinks(content: string, foundLinks: FoundLink[]) {
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error("Could not scan for relation map links: " + e.message);
|
||||
log.error(`Could not scan for relation map links: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,8 +662,8 @@ function saveAttachments(note: BNote, content: string) {
|
||||
|
||||
const attachment = note.saveAttachment({
|
||||
role: "file",
|
||||
mime: mime,
|
||||
title: title,
|
||||
mime,
|
||||
title,
|
||||
content: buffer
|
||||
});
|
||||
|
||||
@@ -953,7 +959,7 @@ function duplicateSubtree(origNoteId: string, newParentNoteId: string) {
|
||||
const duplicateNoteSuffix = t("notes.duplicate-note-suffix");
|
||||
|
||||
if (!res.note.title.endsWith(duplicateNoteSuffix) && !res.note.title.startsWith(duplicateNoteSuffix)) {
|
||||
res.note.title = t("notes.duplicate-note-title", { noteTitle: res.note.title, duplicateNoteSuffix: duplicateNoteSuffix });
|
||||
res.note.title = t("notes.duplicate-note-title", { noteTitle: res.note.title, duplicateNoteSuffix });
|
||||
}
|
||||
|
||||
res.note.save();
|
||||
@@ -1050,13 +1056,12 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch | null | und
|
||||
note: existingNote,
|
||||
branch: createDuplicatedBranch()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
// order here is important, note needs to be created first to not mess up the becca
|
||||
note: createDuplicatedNote(),
|
||||
branch: createDuplicatedBranch()
|
||||
};
|
||||
}
|
||||
return {
|
||||
// order here is important, note needs to be created first to not mess up the becca
|
||||
note: createDuplicatedNote(),
|
||||
branch: createDuplicatedBranch()
|
||||
};
|
||||
}
|
||||
|
||||
function getNoteIdMapping(origNote: BNote) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"preview": "pnpm build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"preact": "10.28.2",
|
||||
"preact-iso": "2.11.1",
|
||||
@@ -23,7 +23,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"user-agent-data-types": "0.4.2",
|
||||
"vite": "7.3.1",
|
||||
"vitest": "4.0.17"
|
||||
"vitest": "4.0.18"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "preact"
|
||||
|
||||
4
docs/README-hi.md
vendored
4
docs/README-hi.md
vendored
@@ -114,8 +114,8 @@ application with focus on building large personal knowledge bases.
|
||||
location pins and GPX tracks
|
||||
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced
|
||||
showcases](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
|
||||
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) for
|
||||
automation
|
||||
* ऑटोमेशन के लिए [REST
|
||||
API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi)
|
||||
* Scales well in both usability and performance upwards of 100 000 notes
|
||||
* Touch optimized [mobile
|
||||
frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for
|
||||
|
||||
48
flake.lock
generated
48
flake.lock
generated
@@ -18,43 +18,26 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765701828,
|
||||
"narHash": "sha256-bUqeCi+mdqXt6Ag0n+9QAqyvFiQPZdSCzTI70Nn3HhA=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1769184885,
|
||||
"narHash": "sha256-wVX5Cqpz66SINNsmt3Bv/Ijzzfl8EPUISq5rKK129K0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "62996354316ef041b26c36e998a9ef193ede2864",
|
||||
"rev": "12689597ba7a6d776c3c979f393896be095269d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "master",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pnpm2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
@@ -94,21 +77,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
description = "Trilium Notes (experimental flake)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/master";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
pnpm2nix = {
|
||||
url = "github:FliegendeWurst/pnpm2nix-nzbr";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs = {
|
||||
flake-utils.follows = "flake-utils";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -51,9 +51,9 @@
|
||||
"@types/express": "5.0.6",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.9",
|
||||
"@vitest/browser-webdriverio": "4.0.17",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@vitest/ui": "4.0.17",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
"chalk": "5.6.2",
|
||||
"cross-env": "10.1.0",
|
||||
"dpdm": "3.14.0",
|
||||
@@ -63,7 +63,7 @@
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-playwright": "2.5.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"happy-dom": "20.3.3",
|
||||
"happy-dom": "20.3.7",
|
||||
"http-server": "14.1.1",
|
||||
"jiti": "2.6.1",
|
||||
"js-yaml": "4.1.1",
|
||||
@@ -77,7 +77,7 @@
|
||||
"upath": "2.0.1",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-dts": "4.5.4",
|
||||
"vitest": "4.0.17"
|
||||
"vitest": "4.0.18"
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": {
|
||||
@@ -93,7 +93,7 @@
|
||||
"url": "https://github.com/TriliumNext/Trilium/issues"
|
||||
},
|
||||
"homepage": "https://triliumnotes.org",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||
@@ -115,7 +115,7 @@
|
||||
"on-headers@<1.1.0": ">=1.1.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"form-data@>=3.0.0 <3.0.4": ">=3.0.4",
|
||||
"node-abi": "4.25.0"
|
||||
"node-abi": "4.26.0"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"sqlite3"
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/coverage-istanbul": "4.0.17",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/coverage-istanbul": "4.0.17",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/coverage-istanbul": "4.0.17",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/coverage-istanbul": "4.0.17",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/coverage-istanbul": "4.0.17",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -71,6 +71,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"lodash-es": "4.17.22"
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ckeditor5-premium-features": "47.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/middleware-retry": "4.4.24",
|
||||
"@smithy/middleware-retry": "4.4.27",
|
||||
"@types/jquery": "3.5.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
|
||||
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig, KeyBinding } from "@codemirror/view";
|
||||
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter, codeFolding } from "@codemirror/language";
|
||||
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
@@ -12,6 +12,17 @@ import { createSearchHighlighter, SearchHighlighter, searchMatchHighlightTheme }
|
||||
|
||||
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
|
||||
|
||||
// Custom keymap to prevent Ctrl+Enter from inserting a newline
|
||||
// This allows the parent application to handle the shortcut (e.g., for "Run Active Note")
|
||||
const preventCtrlEnterKeymap: readonly KeyBinding[] = [
|
||||
{
|
||||
key: "Ctrl-Enter",
|
||||
mac: "Cmd-Enter",
|
||||
run: () => true, // Return true to mark event as handled, preventing default newline insertion
|
||||
preventDefault: true
|
||||
}
|
||||
];
|
||||
|
||||
type ContentChangedListener = () => void;
|
||||
|
||||
export interface EditorConfig {
|
||||
@@ -59,6 +70,7 @@ export default class CodeMirror extends EditorView {
|
||||
lineNumbers(),
|
||||
indentUnit.of(" ".repeat(4)),
|
||||
keymap.of([
|
||||
...preventCtrlEnterKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...smartIndentWithTab
|
||||
|
||||
@@ -13,4 +13,3 @@ export * from "./lib/attribute_names.js";
|
||||
export * from "./lib/utils.js";
|
||||
export * from "./lib/dayjs.js";
|
||||
export * from "./lib/notes.js";
|
||||
export * from "./lib/week_utils.js";
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dayjs } from "./dayjs.js";
|
||||
import { getWeekInfo, getFirstDayOfWeek1, getWeekString, WeekSettings, DEFAULT_WEEK_SETTINGS } from "./week_utils.js";
|
||||
|
||||
describe("week_utils", () => {
|
||||
describe("getWeekInfo", () => {
|
||||
describe("with firstWeekOfYear=0 (first week contains first day of year)", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2026-W01 (cross-year week)", () => {
|
||||
// 2026-01-01 is Thursday, so the week containing it starts on 2025-12-29 (Monday)
|
||||
// This week should be 2026-W01 because it contains 2026-01-01
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2026-W01", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2025-12-28 should be 2025-W52", () => {
|
||||
// 2025-12-28 is Sunday, which is the last day of the week starting 2025-12-22
|
||||
const result = getWeekInfo(dayjs("2025-12-28"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-05 should be 2026-W02", () => {
|
||||
// 2026-01-05 is Monday, start of second week
|
||||
const result = getWeekInfo(dayjs("2026-01-05"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with firstWeekOfYear=1 (ISO standard, first week contains first Thursday)", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 1,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
it("2023-01-01 should be 2022-W52 (Jan 1 is Sunday)", () => {
|
||||
// 2023-01-01 is Sunday, so the week starts on 2022-12-26
|
||||
// Since this week doesn't contain Jan 4, it's 2022-W52
|
||||
const result = getWeekInfo(dayjs("2023-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2022);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2023-01-02 should be 2023-W01 (first Monday)", () => {
|
||||
const result = getWeekInfo(dayjs("2023-01-02"), settings);
|
||||
expect(result.weekYear).toBe(2023);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with firstWeekOfYear=2 (minimum days in first week)", () => {
|
||||
// 2026-01-01 is Thursday
|
||||
// The week containing Jan 1 starts on 2025-12-29 (Monday)
|
||||
// This week has 4 days in 2026 (Thu, Fri, Sat, Sun = Jan 1-4)
|
||||
|
||||
describe("with minDaysInFirstWeek=1", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 2,
|
||||
minDaysInFirstWeek: 1
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2026-W01 (4 days >= 1 minimum)", () => {
|
||||
// Week has 4 days in 2026, which is >= 1
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2026-W01", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with minDaysInFirstWeek=7", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 2,
|
||||
minDaysInFirstWeek: 7
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2025-W52 (4 days < 7 minimum, so this is last week of 2025)", () => {
|
||||
// Week has only 4 days in 2026, which is < 7
|
||||
// So this week belongs to 2025
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2025-W52 (still last week of 2025)", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-05 should be 2026-W01 (first full week of 2026)", () => {
|
||||
// 2026-01-05 is Monday, start of the first full week
|
||||
const result = getWeekInfo(dayjs("2026-01-05"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFirstDayOfWeek1", () => {
|
||||
it("with firstWeekOfYear=0, returns the first day of the week containing Jan 1", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
// 2026-01-01 is Thursday, so week starts on 2025-12-29
|
||||
const result = getFirstDayOfWeek1(2026, settings);
|
||||
expect(result.format("YYYY-MM-DD")).toBe("2025-12-29");
|
||||
});
|
||||
|
||||
it("with firstWeekOfYear=1, returns the first day of the week containing Jan 4", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 1,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
// 2023-01-04 is Wednesday, so week starts on 2023-01-02
|
||||
const result = getFirstDayOfWeek1(2023, settings);
|
||||
expect(result.format("YYYY-MM-DD")).toBe("2023-01-02");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWeekString", () => {
|
||||
it("generates correct week string for cross-year week", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
expect(getWeekString(dayjs("2025-12-29"), settings)).toBe("2026-W01");
|
||||
});
|
||||
|
||||
it("generates correct week string with padded week number", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
expect(getWeekString(dayjs("2026-01-05"), settings)).toBe("2026-W02");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { dayjs, Dayjs } from "./dayjs.js";
|
||||
|
||||
/**
|
||||
* Week settings for calculating week numbers.
|
||||
*/
|
||||
export interface WeekSettings {
|
||||
/** First day of the week (1=Monday to 7=Sunday) */
|
||||
firstDayOfWeek: number;
|
||||
/**
|
||||
* How to determine the first week of the year:
|
||||
* - 0: First week contains first day of the year
|
||||
* - 1: First week contains first Thursday (ISO 8601 standard)
|
||||
* - 2: First week has minimum days
|
||||
*/
|
||||
firstWeekOfYear: number;
|
||||
/** Minimum days in first week (used when firstWeekOfYear=2) */
|
||||
minDaysInFirstWeek: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default week settings (first week contains first day of year, week starts on Monday).
|
||||
*/
|
||||
export const DEFAULT_WEEK_SETTINGS: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the first day of week 1 for a given year, based on user settings.
|
||||
*
|
||||
* @param year The year to calculate for
|
||||
* @param settings Week calculation settings
|
||||
* @returns The first day of week 1
|
||||
*/
|
||||
export function getFirstDayOfWeek1(year: number, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
|
||||
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = settings;
|
||||
|
||||
const jan1 = dayjs(`${year}-01-01`);
|
||||
const jan1Weekday = jan1.isoWeekday(); // 1=Monday, 7=Sunday
|
||||
|
||||
// Calculate the first day of the week containing Jan 1
|
||||
const daysToSubtract = (jan1Weekday - firstDayOfWeek + 7) % 7;
|
||||
const weekContainingJan1Start = jan1.subtract(daysToSubtract, "day");
|
||||
|
||||
if (firstWeekOfYear === 0) {
|
||||
// First week contains first day of the year
|
||||
return weekContainingJan1Start;
|
||||
} else if (firstWeekOfYear === 1) {
|
||||
// First week contains first Thursday (ISO 8601 standard)
|
||||
const jan4 = dayjs(`${year}-01-04`);
|
||||
const jan4Weekday = jan4.isoWeekday();
|
||||
const daysToSubtractFromJan4 = (jan4Weekday - firstDayOfWeek + 7) % 7;
|
||||
return jan4.subtract(daysToSubtractFromJan4, "day");
|
||||
} else {
|
||||
// First week has minimum days
|
||||
const daysInFirstWeek = 7 - daysToSubtract;
|
||||
if (daysInFirstWeek >= minDaysInFirstWeek) {
|
||||
return weekContainingJan1Start;
|
||||
} else {
|
||||
return weekContainingJan1Start.add(1, "week");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the week year and week number for a given date based on user settings.
|
||||
*
|
||||
* @param date The date to calculate week info for
|
||||
* @param settings Week calculation settings
|
||||
* @returns Object with weekYear and weekNumber
|
||||
*/
|
||||
export function getWeekInfo(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): { weekYear: number; weekNumber: number } {
|
||||
const { firstDayOfWeek } = settings;
|
||||
|
||||
// Get the start of the week containing this date
|
||||
const dateWeekday = date.isoWeekday();
|
||||
const daysToSubtract = (dateWeekday - firstDayOfWeek + 7) % 7;
|
||||
const weekStart = date.subtract(daysToSubtract, "day");
|
||||
|
||||
// Try current year first
|
||||
let year = date.year();
|
||||
let firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
|
||||
|
||||
// If the week start is before week 1 of current year, it belongs to previous year
|
||||
if (weekStart.isBefore(firstDayOfWeek1)) {
|
||||
year--;
|
||||
firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
|
||||
} else {
|
||||
// Check if this might belong to next year's week 1
|
||||
const nextYearFirstDayOfWeek1 = getFirstDayOfWeek1(year + 1, settings);
|
||||
if (!weekStart.isBefore(nextYearFirstDayOfWeek1)) {
|
||||
year++;
|
||||
firstDayOfWeek1 = nextYearFirstDayOfWeek1;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate week number
|
||||
const weekNumber = weekStart.diff(firstDayOfWeek1, "week") + 1;
|
||||
|
||||
return { weekYear: year, weekNumber };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a week string in the format "YYYY-Www" (e.g., "2026-W01").
|
||||
*
|
||||
* @param date The date to generate the week string for
|
||||
* @param settings Week calculation settings
|
||||
* @returns Week string in format "YYYY-Www"
|
||||
*/
|
||||
export function getWeekString(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): string {
|
||||
const { weekYear, weekNumber } = getWeekInfo(date, settings);
|
||||
return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start date of the week containing the given date.
|
||||
*
|
||||
* @param date The date to find the week start for
|
||||
* @param firstDayOfWeek First day of the week (1=Monday to 7=Sunday)
|
||||
* @returns The start of the week
|
||||
*/
|
||||
export function getWeekStartDate(date: Dayjs, firstDayOfWeek: number = 1): Dayjs {
|
||||
const dateWeekday = date.isoWeekday();
|
||||
const diff = (dateWeekday - firstDayOfWeek + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a week string and returns the start date of that week.
|
||||
*
|
||||
* @param weekStr Week string in format "YYYY-Www" (e.g., "2026-W01")
|
||||
* @param settings Week calculation settings
|
||||
* @returns The start date of the week
|
||||
*/
|
||||
export function parseWeekString(weekStr: string, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
|
||||
const [yearStr, weekNumStr] = weekStr.trim().split("-W");
|
||||
const weekNumber = parseInt(weekNumStr);
|
||||
const weekYear = parseInt(yearStr);
|
||||
|
||||
const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, settings);
|
||||
return firstDayOfWeek1.add(weekNumber - 1, "week");
|
||||
}
|
||||
@@ -22,7 +22,6 @@ export async function ensureMimeTypes(mimeTypes: MimeType[]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
registeredMimeTypes.add(mime);
|
||||
const loader = syntaxDefinitions[mime];
|
||||
if (!loader) {
|
||||
unsupportedMimeTypes.add(mime);
|
||||
@@ -31,6 +30,7 @@ export async function ensureMimeTypes(mimeTypes: MimeType[]) {
|
||||
|
||||
const language = (await loader()).default;
|
||||
hljs.registerLanguage(mime, language);
|
||||
registeredMimeTypes.add(mime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
808
pnpm-lock.yaml
generated
808
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user