From e6ebfc491c90958ac72c301b8d422ac0ca54ff65 Mon Sep 17 00:00:00 2001 From: contributor Date: Mon, 10 Nov 2025 20:42:56 +0200 Subject: [PATCH 01/19] edited notes: move edited-notes related code to own module moved as-is --- apps/server/src/routes/api/edited-notes.ts | 86 ++++++++++++++++++++++ apps/server/src/routes/api/revisions.ts | 80 +------------------- apps/server/src/routes/routes.ts | 3 +- 3 files changed, 89 insertions(+), 80 deletions(-) create mode 100644 apps/server/src/routes/api/edited-notes.ts diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts new file mode 100644 index 0000000000..953e5b4d08 --- /dev/null +++ b/apps/server/src/routes/api/edited-notes.ts @@ -0,0 +1,86 @@ +import beccaService from "../../becca/becca_service.js"; +import sql from "../../services/sql.js"; +import cls from "../../services/cls.js"; +import becca from "../../becca/becca.js"; +import type { Request } from "express"; +import { NotePojo } from "../../becca/becca-interface.js"; +import type BNote from "../../becca/entities/bnote.js"; +import { EditedNotesResponse } from "@triliumnext/commons"; + +interface NotePath { + noteId: string; + branchId?: string; + title: string; + notePath: string[]; + path: string; +} + +interface NotePojoWithNotePath extends NotePojo { + notePath?: string[] | null; +} + +function getEditedNotesOnDate(req: Request) { + + const noteIds = sql.getColumn(/*sql*/`\ + SELECT notes.* + FROM notes + WHERE noteId IN ( + SELECT noteId FROM notes + WHERE + (notes.dateCreated LIKE :date OR notes.dateModified LIKE :date) + AND (notes.noteId NOT LIKE '\\_%' ESCAPE '\\') + UNION ALL + SELECT noteId FROM revisions + WHERE revisions.dateCreated LIKE :date + ) + ORDER BY isDeleted + LIMIT 50`, + { date: `${req.params.date}%` } + ); + + let notes = becca.getNotes(noteIds, true); + + // Narrow down the results if a note is hoisted, similar to "Jump to note". + const hoistedNoteId = cls.getHoistedNoteId(); + if (hoistedNoteId !== "root") { + notes = notes.filter((note) => note.hasAncestor(hoistedNoteId)); + } + + return notes.map((note) => { + const notePath = getNotePathData(note); + + const notePojo: NotePojoWithNotePath = note.getPojo(); + notePojo.notePath = notePath ? notePath.notePath : null; + + return notePojo; + }) satisfies EditedNotesResponse; +} + +function getNotePathData(note: BNote): NotePath | undefined { + const retPath = note.getBestNotePath(); + + if (retPath) { + const noteTitle = beccaService.getNoteTitleForPath(retPath); + + let branchId; + + if (note.isRoot()) { + branchId = "none_root"; + } else { + const parentNote = note.parents[0]; + branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId)?.branchId; + } + + return { + noteId: note.noteId, + branchId: branchId, + title: noteTitle, + notePath: retPath, + path: retPath.join("/") + }; + } +} + +export default { + getEditedNotesOnDate, +}; diff --git a/apps/server/src/routes/api/revisions.ts b/apps/server/src/routes/api/revisions.ts index 9700e7f782..7780605dad 100644 --- a/apps/server/src/routes/api/revisions.ts +++ b/apps/server/src/routes/api/revisions.ts @@ -1,30 +1,14 @@ "use strict"; -import beccaService from "../../becca/becca_service.js"; import utils from "../../services/utils.js"; import sql from "../../services/sql.js"; -import cls from "../../services/cls.js"; import path from "path"; import becca from "../../becca/becca.js"; import blobService from "../../services/blob.js"; import eraseService from "../../services/erase.js"; import type { Request, Response } from "express"; import type BRevision from "../../becca/entities/brevision.js"; -import type BNote from "../../becca/entities/bnote.js"; -import type { NotePojo } from "../../becca/becca-interface.js"; -import { EditedNotesResponse, RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons"; - -interface NotePath { - noteId: string; - branchId?: string; - title: string; - notePath: string[]; - path: string; -} - -interface NotePojoWithNotePath extends NotePojo { - notePath?: string[] | null; -} +import { RevisionItem, RevisionPojo } from "@triliumnext/commons"; function getRevisionBlob(req: Request) { const preview = req.query.preview === "true"; @@ -151,73 +135,11 @@ function restoreRevision(req: Request) { } } -function getEditedNotesOnDate(req: Request) { - const noteIds = sql.getColumn(/*sql*/`\ - SELECT notes.* - FROM notes - WHERE noteId IN ( - SELECT noteId FROM notes - WHERE - (notes.dateCreated LIKE :date OR notes.dateModified LIKE :date) - AND (notes.noteId NOT LIKE '\\_%' ESCAPE '\\') - UNION ALL - SELECT noteId FROM revisions - WHERE revisions.dateCreated LIKE :date - ) - ORDER BY isDeleted - LIMIT 50`, - { date: `${req.params.date}%` } - ); - - let notes = becca.getNotes(noteIds, true); - - // Narrow down the results if a note is hoisted, similar to "Jump to note". - const hoistedNoteId = cls.getHoistedNoteId(); - if (hoistedNoteId !== "root") { - notes = notes.filter((note) => note.hasAncestor(hoistedNoteId)); - } - - return notes.map((note) => { - const notePath = getNotePathData(note); - - const notePojo: NotePojoWithNotePath = note.getPojo(); - notePojo.notePath = notePath ? notePath.notePath : null; - - return notePojo; - }) satisfies EditedNotesResponse; -} - -function getNotePathData(note: BNote): NotePath | undefined { - const retPath = note.getBestNotePath(); - - if (retPath) { - const noteTitle = beccaService.getNoteTitleForPath(retPath); - - let branchId; - - if (note.isRoot()) { - branchId = "none_root"; - } else { - const parentNote = note.parents[0]; - branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId)?.branchId; - } - - return { - noteId: note.noteId, - branchId: branchId, - title: noteTitle, - notePath: retPath, - path: retPath.join("/") - }; - } -} - export default { getRevisionBlob, getRevisions, getRevision, downloadRevision, - getEditedNotesOnDate, eraseAllRevisions, eraseAllExcessRevisions, eraseRevision, diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 78a1380b73..5b4dae29ac 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -22,6 +22,7 @@ import attachmentsApiRoute from "./api/attachments.js"; import autocompleteApiRoute from "./api/autocomplete.js"; import cloningApiRoute from "./api/cloning.js"; import revisionsApiRoute from "./api/revisions.js"; +import editedNotesApiRoute from "./api/edited-notes.js"; import recentChangesApiRoute from "./api/recent_changes.js"; import optionsApiRoute from "./api/options.js"; import passwordApiRoute from "./api/password.js"; @@ -349,7 +350,7 @@ function register(app: express.Application) { apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage); apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown); apiRoute(GET, "/api/recent-changes/:ancestorNoteId", recentChangesApiRoute.getRecentChanges); - apiRoute(GET, "/api/edited-notes/:date", revisionsApiRoute.getEditedNotesOnDate); + apiRoute(GET, "/api/edited-notes/:date", editedNotesApiRoute.getEditedNotesOnDate); apiRoute(PST, "/api/note-map/:noteId/tree", noteMapRoute.getTreeMap); apiRoute(PST, "/api/note-map/:noteId/link", noteMapRoute.getLinkMap); From dd7c556820835d5d681b4415c0a1059966440007 Mon Sep 17 00:00:00 2001 From: contributor Date: Tue, 11 Nov 2025 19:54:51 +0200 Subject: [PATCH 02/19] edited notes: recognize dateNote label value TODAY, MONTH, YEAR --- .../src/routes/api/edited-notes.spec.ts | 76 +++++++++++++++++++ apps/server/src/routes/api/edited-notes.ts | 53 ++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/routes/api/edited-notes.spec.ts diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts new file mode 100644 index 0000000000..29fd47e18b --- /dev/null +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import dayjs from "dayjs"; +import { resolveDateParams } from "./edited-notes.js"; + +function resolveAsDate(dateStr: string) { + return resolveDateParams(dateStr).date; +} + +describe("edited-notes::resolveAsDate", () => { + beforeEach(() => { + // Set a fixed date and time before each test + vi.useFakeTimers(); + vi.setSystemTime(new Date('2012-11-10T23:22:21Z')); // NOTE!!: Date wrap in my timezone + }); + + afterEach(() => { + // Restore real timers after each test + vi.useRealTimers(); + }); + + + it("resolves 'TODAY' to today's date", () => { + const expectedDate = dayjs().format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("TODAY"); + expect(resolvedDate).toBe(expectedDate); + }); + + it("resolves 'MONTH' to current month", () => { + const expectedMonth = dayjs().format("YYYY-MM"); + const resolvedMonth = resolveAsDate("MONTH"); + expect(resolvedMonth).toBe(expectedMonth); + }); + + it("resolves 'YEAR' to current year", () => { + const expectedYear = dayjs().format("YYYY"); + const resolvedYear = resolveAsDate("YEAR"); + expect(resolvedYear).toBe(expectedYear); + }); + + it("resolves 'TODAY-1' to yesterday's date", () => { + const expectedDate = dayjs().subtract(1, "day").format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("TODAY-1"); + expect(resolvedDate).toBe(expectedDate); + }); + + it("resolves 'MONTH-2' to 2 months ago", () => { + const expectedMonth = dayjs().subtract(2, "month").format("YYYY-MM"); + const resolvedMonth = resolveAsDate("MONTH-2"); + expect(resolvedMonth).toBe(expectedMonth); + }); + + it("resolves 'YEAR+1' to next year", () => { + const expectedYear = dayjs().add(1, "year").format("YYYY"); + const resolvedYear = resolveAsDate("YEAR+1"); + expect(resolvedYear).toBe(expectedYear); + }); + + it("returns original string for unrecognized keyword", () => { + const unrecognizedString = "NOT_A_DYNAMIC_DATE"; + const resolvedString = resolveAsDate(unrecognizedString); + expect(resolvedString).toBe(unrecognizedString); + }); + + it("returns original string for partially recognized keyword", () => { + const partialString = "TODAY-"; + const resolvedString = resolveAsDate(partialString); + expect(resolvedString).toBe(partialString); + }); + + it("resolves 'today' (lowercase) to today's date", () => { + const expectedDate = dayjs().format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("today"); + expect(resolvedDate).toBe(expectedDate); + }); + +}); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 953e5b4d08..22bd45158e 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -6,6 +6,7 @@ import type { Request } from "express"; import { NotePojo } from "../../becca/becca-interface.js"; import type BNote from "../../becca/entities/bnote.js"; import { EditedNotesResponse } from "@triliumnext/commons"; +import dayjs from "dayjs"; interface NotePath { noteId: string; @@ -20,6 +21,9 @@ interface NotePojoWithNotePath extends NotePojo { } function getEditedNotesOnDate(req: Request) { + const resolvedDateParams = resolveDateParams(req.params.date); + + const sqlParams = { date: resolvedDateParams.date + "%" }; const noteIds = sql.getColumn(/*sql*/`\ SELECT notes.* @@ -35,7 +39,7 @@ function getEditedNotesOnDate(req: Request) { ) ORDER BY isDeleted LIMIT 50`, - { date: `${req.params.date}%` } + sqlParams ); let notes = becca.getNotes(noteIds, true); @@ -81,6 +85,53 @@ function getNotePathData(note: BNote): NotePath | undefined { } } +function formatDateFromKeywordAndDelta(keyword: string, delta: number): string { + const formatMap = new Map([ + ["today", { format: "YYYY-MM-DD", addUnit: "day" }], + ["month", { format: "YYYY-MM", addUnit: "month" }], + ["year", { format: "YYYY", addUnit: "year" }] + ]); + + const handler = formatMap.get(keyword); + + if (!handler) { + throw new Error(`Unrecognized keyword: ${keyword}`); + } + + const date = dayjs().add(delta, handler.addUnit); + return date.format(handler.format); +} + +interface DateValue { + // kind: "date", + date: string, +} + +type DateFilter = DateValue; + +/** + * Resolves date keyword with optional delta (e.g., "TODAY-1") to date + * @param dateStr date keyword (TODAY, MONTH, YEAR) or date in format YYYY-MM-DD (or beggining) + * @returns + */ +export function resolveDateParams(dateStr: string): DateFilter { + const match = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); + + if (!match) { + return { + date: `${dateStr}` + } + } + + const keyword = match[1].toLowerCase(); + const delta = match[2] ? parseInt(match[2]) : 0; + + const date = formatDateFromKeywordAndDelta(keyword, delta); + return { + date: `${date}` + } +} + export default { getEditedNotesOnDate, }; From 8e989cfb16f568cf58558fcd8f7f3f36fbaf8530 Mon Sep 17 00:00:00 2001 From: contributor Date: Tue, 11 Nov 2025 19:27:06 +0200 Subject: [PATCH 03/19] edited notes: handle timezone differences between client and server --- .../src/routes/api/edited-notes.spec.ts | 69 +++++++++---------- apps/server/src/routes/api/edited-notes.ts | 10 +-- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index 29fd47e18b..e92a9e6349 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -1,16 +1,34 @@ +import cls from '../../services/cls.js'; import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; -import dayjs from "dayjs"; import { resolveDateParams } from "./edited-notes.js"; -function resolveAsDate(dateStr: string) { - return resolveDateParams(dateStr).date; +// test date setup +// client: UTC+1 +// server: UTC +// day/month/year is changed when server converts a client date to to UTC +const clientDate = "2025-01-01 00:11:11.000+0100"; +const serverDate = "2024-12-31 23:11:11.000Z"; + +// expected values - from client's point of view +const expectedToday = "2025-01-01"; +const expectedTodayMinus1 = "2024-12-31"; +const expectedMonth = "2025-01"; +const expectedMonthMinus2 = "2024-11"; +const expectedYear = "2025"; +const expectedYearMinus1 = "2024"; + +function runTest(dateStrToResolve: string, expectedDate: string) { + cls.init(() => { + cls.set("localNowDateTime", clientDate); + const resolvedDate = resolveDateParams(dateStrToResolve).date; + expect(resolvedDate).toBe(expectedDate); + }); } -describe("edited-notes::resolveAsDate", () => { +describe("edited-notes::resolveDateParams", () => { beforeEach(() => { - // Set a fixed date and time before each test vi.useFakeTimers(); - vi.setSystemTime(new Date('2012-11-10T23:22:21Z')); // NOTE!!: Date wrap in my timezone + vi.setSystemTime(new Date(serverDate)); }); afterEach(() => { @@ -18,59 +36,40 @@ describe("edited-notes::resolveAsDate", () => { vi.useRealTimers(); }); - it("resolves 'TODAY' to today's date", () => { - const expectedDate = dayjs().format("YYYY-MM-DD"); - const resolvedDate = resolveAsDate("TODAY"); - expect(resolvedDate).toBe(expectedDate); + runTest("TODAY", expectedToday); }); it("resolves 'MONTH' to current month", () => { - const expectedMonth = dayjs().format("YYYY-MM"); - const resolvedMonth = resolveAsDate("MONTH"); - expect(resolvedMonth).toBe(expectedMonth); + runTest("MONTH", expectedMonth); }); it("resolves 'YEAR' to current year", () => { - const expectedYear = dayjs().format("YYYY"); - const resolvedYear = resolveAsDate("YEAR"); - expect(resolvedYear).toBe(expectedYear); + runTest("YEAR", expectedYear); }); it("resolves 'TODAY-1' to yesterday's date", () => { - const expectedDate = dayjs().subtract(1, "day").format("YYYY-MM-DD"); - const resolvedDate = resolveAsDate("TODAY-1"); - expect(resolvedDate).toBe(expectedDate); + runTest("TODAY-1", expectedTodayMinus1); }); it("resolves 'MONTH-2' to 2 months ago", () => { - const expectedMonth = dayjs().subtract(2, "month").format("YYYY-MM"); - const resolvedMonth = resolveAsDate("MONTH-2"); - expect(resolvedMonth).toBe(expectedMonth); + runTest("MONTH-2", expectedMonthMinus2); }); - it("resolves 'YEAR+1' to next year", () => { - const expectedYear = dayjs().add(1, "year").format("YYYY"); - const resolvedYear = resolveAsDate("YEAR+1"); - expect(resolvedYear).toBe(expectedYear); + it("resolves 'YEAR-1' to last year", () => { + runTest("YEAR-1", expectedYearMinus1); }); it("returns original string for unrecognized keyword", () => { - const unrecognizedString = "NOT_A_DYNAMIC_DATE"; - const resolvedString = resolveAsDate(unrecognizedString); - expect(resolvedString).toBe(unrecognizedString); + runTest("FOO", "FOO"); }); it("returns original string for partially recognized keyword", () => { - const partialString = "TODAY-"; - const resolvedString = resolveAsDate(partialString); - expect(resolvedString).toBe(partialString); + runTest("TODAY-", "TODAY-"); }); it("resolves 'today' (lowercase) to today's date", () => { - const expectedDate = dayjs().format("YYYY-MM-DD"); - const resolvedDate = resolveAsDate("today"); - expect(resolvedDate).toBe(expectedDate); + runTest("today", expectedToday); }); }); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 22bd45158e..06b6690cec 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import beccaService from "../../becca/becca_service.js"; import sql from "../../services/sql.js"; import cls from "../../services/cls.js"; @@ -6,7 +7,7 @@ import type { Request } from "express"; import { NotePojo } from "../../becca/becca-interface.js"; import type BNote from "../../becca/entities/bnote.js"; import { EditedNotesResponse } from "@triliumnext/commons"; -import dayjs from "dayjs"; +import dateUtils from "../../services/date_utils.js"; interface NotePath { noteId: string; @@ -85,7 +86,7 @@ function getNotePathData(note: BNote): NotePath | undefined { } } -function formatDateFromKeywordAndDelta(keyword: string, delta: number): string { +function formatDateFromKeywordAndDelta(startingDate: dayjs.Dayjs, keyword: string, delta: number): string { const formatMap = new Map([ ["today", { format: "YYYY-MM-DD", addUnit: "day" }], ["month", { format: "YYYY-MM", addUnit: "month" }], @@ -98,7 +99,7 @@ function formatDateFromKeywordAndDelta(keyword: string, delta: number): string { throw new Error(`Unrecognized keyword: ${keyword}`); } - const date = dayjs().add(delta, handler.addUnit); + const date = startingDate.add(delta, handler.addUnit); return date.format(handler.format); } @@ -126,7 +127,8 @@ export function resolveDateParams(dateStr: string): DateFilter { const keyword = match[1].toLowerCase(); const delta = match[2] ? parseInt(match[2]) : 0; - const date = formatDateFromKeywordAndDelta(keyword, delta); + const clientDate = dayjs(dateUtils.localNowDate()); + const date = formatDateFromKeywordAndDelta(clientDate, keyword, delta); return { date: `${date}` } From 8e09ed8712d0aa34d60da6a6d7672c642c103f07 Mon Sep 17 00:00:00 2001 From: contributor Date: Tue, 11 Nov 2025 20:00:47 +0200 Subject: [PATCH 04/19] edited notes: force tests to run in UTC timezone --- apps/server/src/routes/api/edited-notes.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index e92a9e6349..a3f022e377 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -27,11 +27,13 @@ function runTest(dateStrToResolve: string, expectedDate: string) { describe("edited-notes::resolveDateParams", () => { beforeEach(() => { + vi.stubEnv('TZ', 'UTC'); vi.useFakeTimers(); vi.setSystemTime(new Date(serverDate)); }); afterEach(() => { + vi.unstubAllEnvs(); // Restore real timers after each test vi.useRealTimers(); }); From 674528bec867b89510ff0b404d44a477729246f4 Mon Sep 17 00:00:00 2001 From: contributor Date: Wed, 12 Nov 2025 10:43:22 +0200 Subject: [PATCH 05/19] edited notes: better docstring for resolveDateParams --- apps/server/src/routes/api/edited-notes.ts | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 06b6690cec..03bebf6050 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -111,9 +111,27 @@ interface DateValue { type DateFilter = DateValue; /** - * Resolves date keyword with optional delta (e.g., "TODAY-1") to date - * @param dateStr date keyword (TODAY, MONTH, YEAR) or date in format YYYY-MM-DD (or beggining) - * @returns + * Resolves a date string into a concrete date representation. + * The date string can be a keyword with an optional delta, or a standard date format. + * + * Supported keywords are: + * - `today`: Resolves to the current date in `YYYY-MM-DD` format. + * - `month`: Resolves to the current month in `YYYY-MM` format. + * - `year`: Resolves to the current year in `YYYY` format. + * + * An optional delta can be appended to the keyword to specify an offset. + * For example: + * - `today-1` resolves to yesterday. + * - `month+2` resolves to the month after next. + * - `year-10` resolves to 10 years ago. + * + * If the `dateStr` does not match a keyword pattern, it is returned as is. + * This is to support standard date formats like `YYYY-MM-DD`, `YYYY-MM`, or `YYYY`. + * + * @param dateStr A string representing the date. This can be a keyword + * (e.g., "today", "month-1", "year+5") or a date string + * (e.g., "2023-10-27", "2023-10", "2023"). + * @returns A `DateFilter` object containing the resolved date string. */ export function resolveDateParams(dateStr: string): DateFilter { const match = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); From 5557cd4f65470d21f859ebdf8757671bdbcf47da Mon Sep 17 00:00:00 2001 From: contributor Date: Wed, 12 Nov 2025 10:52:16 +0200 Subject: [PATCH 06/19] lint format --- apps/server/src/routes/api/edited-notes.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 03bebf6050..d769f9c4bb 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -86,7 +86,11 @@ function getNotePathData(note: BNote): NotePath | undefined { } } -function formatDateFromKeywordAndDelta(startingDate: dayjs.Dayjs, keyword: string, delta: number): string { +function formatDateFromKeywordAndDelta( + startingDate: dayjs.Dayjs, + keyword: string, + delta: number +): string { const formatMap = new Map([ ["today", { format: "YYYY-MM-DD", addUnit: "day" }], ["month", { format: "YYYY-MM", addUnit: "month" }], From 3379a8fa3961d6014468376ccd090169703d73c8 Mon Sep 17 00:00:00 2001 From: contributor Date: Wed, 12 Nov 2025 14:45:16 +0200 Subject: [PATCH 07/19] edited notes: extract sqlquery var for eslint to recognize indentation --- apps/server/src/routes/api/edited-notes.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index d769f9c4bb..1491cf12d9 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -25,8 +25,7 @@ function getEditedNotesOnDate(req: Request) { const resolvedDateParams = resolveDateParams(req.params.date); const sqlParams = { date: resolvedDateParams.date + "%" }; - - const noteIds = sql.getColumn(/*sql*/`\ + const sqlQuery = /*sql*/`\ SELECT notes.* FROM notes WHERE noteId IN ( @@ -39,7 +38,10 @@ function getEditedNotesOnDate(req: Request) { WHERE revisions.dateCreated LIKE :date ) ORDER BY isDeleted - LIMIT 50`, + LIMIT 50`; + + const noteIds = sql.getColumn( + sqlQuery, sqlParams ); From d167caa6ed3cfaad8aca204dd0a7719d0161274a Mon Sep 17 00:00:00 2001 From: contributor Date: Wed, 12 Nov 2025 16:41:26 +0200 Subject: [PATCH 08/19] edited notes: move formatMap on module level --- apps/server/src/routes/api/edited-notes.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 1491cf12d9..c1efcefc43 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -88,17 +88,17 @@ function getNotePathData(note: BNote): NotePath | undefined { } } +const formatMap = new Map([ + ["today", { format: "YYYY-MM-DD", addUnit: "day" }], + ["month", { format: "YYYY-MM", addUnit: "month" }], + ["year", { format: "YYYY", addUnit: "year" }] +]); + function formatDateFromKeywordAndDelta( startingDate: dayjs.Dayjs, keyword: string, delta: number ): string { - const formatMap = new Map([ - ["today", { format: "YYYY-MM-DD", addUnit: "day" }], - ["month", { format: "YYYY-MM", addUnit: "month" }], - ["year", { format: "YYYY", addUnit: "year" }] - ]); - const handler = formatMap.get(keyword); if (!handler) { @@ -144,7 +144,7 @@ export function resolveDateParams(dateStr: string): DateFilter { if (!match) { return { - date: `${dateStr}` + date: dateStr } } @@ -154,7 +154,7 @@ export function resolveDateParams(dateStr: string): DateFilter { const clientDate = dayjs(dateUtils.localNowDate()); const date = formatDateFromKeywordAndDelta(clientDate, keyword, delta); return { - date: `${date}` + date: date } } From 7aff5d1dba0e0f988d0c6192435eb37207e5e309 Mon Sep 17 00:00:00 2001 From: contributor Date: Wed, 12 Nov 2025 01:14:16 +0200 Subject: [PATCH 09/19] edited notes: extendable EditedNotesResponse this allows to return additional field along with notes, for example, a flag to indicate if response was truncated by limit --- apps/client/src/widgets/ribbon/EditedNotesTab.tsx | 8 ++++---- apps/server/src/routes/api/edited-notes.ts | 10 +++++++--- packages/commons/src/lib/server_api.ts | 8 +++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/ribbon/EditedNotesTab.tsx b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx index 4bdae4126b..b4fd1a19e7 100644 --- a/apps/client/src/widgets/ribbon/EditedNotesTab.tsx +++ b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "preact/hooks"; import { TabContext } from "./ribbon-interface"; -import { EditedNotesResponse } from "@triliumnext/commons"; +import { EditedNotesResponse, EditedNotes } from "@triliumnext/commons"; import server from "../../services/server"; import { t } from "../../services/i18n"; import froca from "../../services/froca"; @@ -8,12 +8,12 @@ import NoteLink from "../react/NoteLink"; import { joinElements } from "../react/react_utils"; export default function EditedNotesTab({ note }: TabContext) { - const [ editedNotes, setEditedNotes ] = useState(); + const [ editedNotes, setEditedNotes ] = useState(); useEffect(() => { if (!note) return; - server.get(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => { - editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId); + server.get(`edited-notes/${note.getLabelValue("dateNote")}`).then(async response => { + const editedNotes = response.notes.filter((n) => n.noteId !== note.noteId); const noteIds = editedNotes.flatMap((n) => n.noteId); await froca.getNotes(noteIds, true); // preload all at once setEditedNotes(editedNotes); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index c1efcefc43..050956ff48 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -6,7 +6,7 @@ import becca from "../../becca/becca.js"; import type { Request } from "express"; import { NotePojo } from "../../becca/becca-interface.js"; import type BNote from "../../becca/entities/bnote.js"; -import { EditedNotesResponse } from "@triliumnext/commons"; +import { EditedNotes, EditedNotesResponse } from "@triliumnext/commons"; import dateUtils from "../../services/date_utils.js"; interface NotePath { @@ -53,14 +53,18 @@ function getEditedNotesOnDate(req: Request) { notes = notes.filter((note) => note.hasAncestor(hoistedNoteId)); } - return notes.map((note) => { + const editedNotes = notes.map((note) => { const notePath = getNotePathData(note); const notePojo: NotePojoWithNotePath = note.getPojo(); notePojo.notePath = notePath ? notePath.notePath : null; return notePojo; - }) satisfies EditedNotesResponse; + }); + + return { + notes: editedNotes, + } satisfies EditedNotesResponse; } function getNotePathData(note: BNote): NotePath | undefined { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index dc6dc4d412..97a3ae1801 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -164,11 +164,17 @@ export type ToggleInParentResponse = { } export type EditedNotesResponse = { + notes: EditedNotes, +} + +export type EditedNote = { noteId: string; isDeleted: boolean; title?: string; notePath?: string[] | null; -}[]; +}; + +export type EditedNotes = EditedNote[]; export interface MetadataResponse { dateCreated: string | undefined; From c389a1207cca1dfeca74b683fe6d3b9265885131 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 14:04:00 +0200 Subject: [PATCH 10/19] edited notes: return limit in response --- apps/server/src/routes/api/edited-notes.ts | 4 +++- packages/commons/src/lib/server_api.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 050956ff48..0e5d5a8816 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -25,6 +25,7 @@ function getEditedNotesOnDate(req: Request) { const resolvedDateParams = resolveDateParams(req.params.date); const sqlParams = { date: resolvedDateParams.date + "%" }; + const limit = 50; const sqlQuery = /*sql*/`\ SELECT notes.* FROM notes @@ -38,7 +39,7 @@ function getEditedNotesOnDate(req: Request) { WHERE revisions.dateCreated LIKE :date ) ORDER BY isDeleted - LIMIT 50`; + LIMIT ${limit}`; const noteIds = sql.getColumn( sqlQuery, @@ -64,6 +65,7 @@ function getEditedNotesOnDate(req: Request) { return { notes: editedNotes, + limit: limit, } satisfies EditedNotesResponse; } diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 97a3ae1801..7f28fa395f 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -165,6 +165,7 @@ export type ToggleInParentResponse = { export type EditedNotesResponse = { notes: EditedNotes, + limit: number } export type EditedNote = { From 3ba6d14c82b713192c8d065987a2e85a57ae0aa0 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 14:09:56 +0200 Subject: [PATCH 11/19] edited notes: add happy path tests --- apps/server/src/routes/api/edited-notes.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index a3f022e377..fbc0d0842f 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -62,6 +62,18 @@ describe("edited-notes::resolveDateParams", () => { runTest("YEAR-1", expectedYearMinus1); }); + it("returns original string for day", () => { + runTest("2020-12-31", "2020-12-31"); + }); + + it("returns original string for month", () => { + runTest("2020-12", "2020-12"); + }); + + it("returns original string for year", () => { + runTest("2020", "2020"); + }); + it("returns original string for unrecognized keyword", () => { runTest("FOO", "FOO"); }); From b54411af1152d462f6d8a9928a5b77bae213f0c5 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 14:13:55 +0200 Subject: [PATCH 12/19] edited notes: more descriptive name dateNoteLabelKeywordToDateFilter --- apps/server/src/routes/api/edited-notes.spec.ts | 8 ++++---- apps/server/src/routes/api/edited-notes.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index fbc0d0842f..dae79302b4 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -1,6 +1,6 @@ import cls from '../../services/cls.js'; import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; -import { resolveDateParams } from "./edited-notes.js"; +import { dateNoteLabelKeywordToDateFilter } from "./edited-notes.js"; // test date setup // client: UTC+1 @@ -20,12 +20,12 @@ const expectedYearMinus1 = "2024"; function runTest(dateStrToResolve: string, expectedDate: string) { cls.init(() => { cls.set("localNowDateTime", clientDate); - const resolvedDate = resolveDateParams(dateStrToResolve).date; - expect(resolvedDate).toBe(expectedDate); + const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrToResolve); + expect(dateFilter.date).toBe(expectedDate); }); } -describe("edited-notes::resolveDateParams", () => { +describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { beforeEach(() => { vi.stubEnv('TZ', 'UTC'); vi.useFakeTimers(); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 0e5d5a8816..171e196499 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -22,9 +22,9 @@ interface NotePojoWithNotePath extends NotePojo { } function getEditedNotesOnDate(req: Request) { - const resolvedDateParams = resolveDateParams(req.params.date); + const dateFilter = dateNoteLabelKeywordToDateFilter(req.params.date); - const sqlParams = { date: resolvedDateParams.date + "%" }; + const sqlParams = { date: dateFilter.date + "%" }; const limit = 50; const sqlQuery = /*sql*/`\ SELECT notes.* @@ -145,7 +145,7 @@ type DateFilter = DateValue; * (e.g., "2023-10-27", "2023-10", "2023"). * @returns A `DateFilter` object containing the resolved date string. */ -export function resolveDateParams(dateStr: string): DateFilter { +export function dateNoteLabelKeywordToDateFilter(dateStr: string): DateFilter { const match = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); if (!match) { From ad9a058ab74e87950b287aed05eb7d4c741dc200 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 14:27:07 +0200 Subject: [PATCH 13/19] edited notes: better names in tests --- .../src/routes/api/edited-notes.spec.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index dae79302b4..3efd119a73 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -17,10 +17,10 @@ const expectedMonthMinus2 = "2024-11"; const expectedYear = "2025"; const expectedYearMinus1 = "2024"; -function runTest(dateStrToResolve: string, expectedDate: string) { +function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) { cls.init(() => { cls.set("localNowDateTime", clientDate); - const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrToResolve); + const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword); expect(dateFilter.date).toBe(expectedDate); }); } @@ -39,51 +39,51 @@ describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { }); it("resolves 'TODAY' to today's date", () => { - runTest("TODAY", expectedToday); + keywordResolvesToDate("TODAY", expectedToday); }); it("resolves 'MONTH' to current month", () => { - runTest("MONTH", expectedMonth); + keywordResolvesToDate("MONTH", expectedMonth); }); it("resolves 'YEAR' to current year", () => { - runTest("YEAR", expectedYear); + keywordResolvesToDate("YEAR", expectedYear); }); it("resolves 'TODAY-1' to yesterday's date", () => { - runTest("TODAY-1", expectedTodayMinus1); + keywordResolvesToDate("TODAY-1", expectedTodayMinus1); }); it("resolves 'MONTH-2' to 2 months ago", () => { - runTest("MONTH-2", expectedMonthMinus2); + keywordResolvesToDate("MONTH-2", expectedMonthMinus2); }); it("resolves 'YEAR-1' to last year", () => { - runTest("YEAR-1", expectedYearMinus1); + keywordResolvesToDate("YEAR-1", expectedYearMinus1); }); it("returns original string for day", () => { - runTest("2020-12-31", "2020-12-31"); + keywordResolvesToDate("2020-12-31", "2020-12-31"); }); it("returns original string for month", () => { - runTest("2020-12", "2020-12"); + keywordResolvesToDate("2020-12", "2020-12"); }); it("returns original string for year", () => { - runTest("2020", "2020"); + keywordResolvesToDate("2020", "2020"); }); it("returns original string for unrecognized keyword", () => { - runTest("FOO", "FOO"); + keywordResolvesToDate("FOO", "FOO"); }); it("returns original string for partially recognized keyword", () => { - runTest("TODAY-", "TODAY-"); + keywordResolvesToDate("TODAY-", "TODAY-"); }); it("resolves 'today' (lowercase) to today's date", () => { - runTest("today", expectedToday); + keywordResolvesToDate("today", expectedToday); }); }); From bcff9291c439b80e60934c7675313d1a9b55fcc5 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 17:56:34 +0200 Subject: [PATCH 14/19] edited notes: recognize not valid dates/keywords, return 0 limit to client --- .../src/routes/api/edited-notes.spec.ts | 32 +++++++++++-- apps/server/src/routes/api/edited-notes.ts | 47 +++++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index 3efd119a73..2596a15614 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -25,6 +25,14 @@ function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) { }); } +function keywordDoesNotResolve(dateStrOrKeyword: string) { + cls.init(() => { + cls.set("localNowDateTime", clientDate); + const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword); + expect(dateFilter.date).toBe(null); + }); +} + describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { beforeEach(() => { vi.stubEnv('TZ', 'UTC'); @@ -70,16 +78,32 @@ describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { keywordResolvesToDate("2020-12", "2020-12"); }); + it("returns original string for partial month", () => { + keywordResolvesToDate("2020-1", "2020-1"); + }); + + it("returns original string for partial month with trailing dash", () => { + keywordResolvesToDate("2020-", "2020-"); + }); + it("returns original string for year", () => { keywordResolvesToDate("2020", "2020"); }); - it("returns original string for unrecognized keyword", () => { - keywordResolvesToDate("FOO", "FOO"); + it("returns original string for potentially partial day", () => { + keywordResolvesToDate("2020-12-1", "2020-12-1"); + }); + + it("returns null for partial year", () => { + keywordDoesNotResolve("202"); + }); + + it("returns null for arbitrary string", () => { + keywordDoesNotResolve("FOO"); }); - it("returns original string for partially recognized keyword", () => { - keywordResolvesToDate("TODAY-", "TODAY-"); + it("returns null for missing delta", () => { + keywordDoesNotResolve("TODAY-"); }); it("resolves 'today' (lowercase) to today's date", () => { diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 171e196499..2cc0d87130 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -24,6 +24,13 @@ interface NotePojoWithNotePath extends NotePojo { function getEditedNotesOnDate(req: Request) { const dateFilter = dateNoteLabelKeywordToDateFilter(req.params.date); + if (!dateFilter.date) { + return { + notes: [], + limit: 0, + } satisfies EditedNotesResponse; + } + const sqlParams = { date: dateFilter.date + "%" }; const limit = 50; const sqlQuery = /*sql*/`\ @@ -117,7 +124,7 @@ function formatDateFromKeywordAndDelta( interface DateValue { // kind: "date", - date: string, + date: string | null, } type DateFilter = DateValue; @@ -146,22 +153,42 @@ type DateFilter = DateValue; * @returns A `DateFilter` object containing the resolved date string. */ export function dateNoteLabelKeywordToDateFilter(dateStr: string): DateFilter { - const match = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); + const keywordAndDelta = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); + + if (keywordAndDelta) { + const keyword = keywordAndDelta[1].toLowerCase(); + const delta = keywordAndDelta[2] ? parseInt(keywordAndDelta[2]) : 0; - if (!match) { + const clientDate = dayjs(dateUtils.localNowDate()); + const date = formatDateFromKeywordAndDelta(clientDate, keyword, delta); + return { + date: date + }; + } + + // Check if it's a valid date format (YYYY-MM-DD, YYYY-MM, or YYYY) + const isDatePrefix = isValidDatePrefix(dateStr); + + if (isDatePrefix) { return { date: dateStr + }; + } else { + // Not a keyword and not a valid date prefix + return { + date: null } } +} - const keyword = match[1].toLowerCase(); - const delta = match[2] ? parseInt(match[2]) : 0; - - const clientDate = dayjs(dateUtils.localNowDate()); - const date = formatDateFromKeywordAndDelta(clientDate, keyword, delta); - return { - date: date +function isValidDatePrefix(dateStr: string): boolean { + // Check if it starts with YYYY format + if (/^\d{4}/.test(dateStr)) { + const year = parseInt(dateStr.substring(0, 4)); + return !isNaN(year) && year > 0 && year < 10000; } + + return false; } export default { From 25844d6b425926a77e35a61c5f193e7ccdb09788 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 20:21:15 +0200 Subject: [PATCH 15/19] edited notes: add positive delta test https://github.com/TriliumNext/Trilium/pull/7704#discussion_r2532135843 --- apps/server/src/routes/api/edited-notes.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index 2596a15614..7227a4c80b 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -12,6 +12,7 @@ const serverDate = "2024-12-31 23:11:11.000Z"; // expected values - from client's point of view const expectedToday = "2025-01-01"; const expectedTodayMinus1 = "2024-12-31"; +const expectedTodayPlus1 = "2025-01-02"; const expectedMonth = "2025-01"; const expectedMonthMinus2 = "2024-11"; const expectedYear = "2025"; @@ -50,6 +51,10 @@ describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { keywordResolvesToDate("TODAY", expectedToday); }); + it("resolves 'TODAY+1' to tomorrow's date", () => { + keywordResolvesToDate("TODAY+1", expectedTodayPlus1); + }); + it("resolves 'MONTH' to current month", () => { keywordResolvesToDate("MONTH", expectedMonth); }); From 405fe0e44c0149b64d1c6c92cc691a1775ea7557 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 20:22:48 +0200 Subject: [PATCH 16/19] edited notes: more restrictive check https://github.com/TriliumNext/Trilium/pull/7704#discussion_r2532135841 --- apps/server/src/routes/api/edited-notes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 2cc0d87130..f4f47febaa 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -182,8 +182,8 @@ export function dateNoteLabelKeywordToDateFilter(dateStr: string): DateFilter { } function isValidDatePrefix(dateStr: string): boolean { - // Check if it starts with YYYY format - if (/^\d{4}/.test(dateStr)) { + // Check if it starts with YYYY format and only contains numbers and dashes afterwards + if (/^\d{4}[-\d]*$/.test(dateStr)) { const year = parseInt(dateStr.substring(0, 4)); return !isNaN(year) && year > 0 && year < 10000; } From cbdca3c24b016c19e929763b5e0b7581e767e5d4 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 20:27:04 +0200 Subject: [PATCH 17/19] edited notes: do not shadow variable https://github.com/TriliumNext/Trilium/pull/7704#discussion_r2532135842 --- apps/client/src/widgets/ribbon/EditedNotesTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/ribbon/EditedNotesTab.tsx b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx index b4fd1a19e7..5e0f2095eb 100644 --- a/apps/client/src/widgets/ribbon/EditedNotesTab.tsx +++ b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx @@ -13,10 +13,10 @@ export default function EditedNotesTab({ note }: TabContext) { useEffect(() => { if (!note) return; server.get(`edited-notes/${note.getLabelValue("dateNote")}`).then(async response => { - const editedNotes = response.notes.filter((n) => n.noteId !== note.noteId); - const noteIds = editedNotes.flatMap((n) => n.noteId); + const filteredNotes = response.notes.filter((n) => n.noteId !== note.noteId); + const noteIds = filteredNotes.flatMap((n) => n.noteId); await froca.getNotes(noteIds, true); // preload all at once - setEditedNotes(editedNotes); + setEditedNotes(filteredNotes); }); }, [ note?.noteId ]); From 2486d9785d82da56339cd776aee7b0317f9d4995 Mon Sep 17 00:00:00 2001 From: contributor Date: Sun, 16 Nov 2025 21:07:09 +0200 Subject: [PATCH 18/19] edited notes: use parameterized limit --- apps/server/src/routes/api/edited-notes.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index f4f47febaa..262db76cbf 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -31,8 +31,10 @@ function getEditedNotesOnDate(req: Request) { } satisfies EditedNotesResponse; } - const sqlParams = { date: dateFilter.date + "%" }; - const limit = 50; + const sqlParams = { + date: dateFilter.date + "%", + limit: 50, + }; const sqlQuery = /*sql*/`\ SELECT notes.* FROM notes @@ -46,7 +48,7 @@ function getEditedNotesOnDate(req: Request) { WHERE revisions.dateCreated LIKE :date ) ORDER BY isDeleted - LIMIT ${limit}`; + LIMIT :limit`; const noteIds = sql.getColumn( sqlQuery, @@ -72,7 +74,7 @@ function getEditedNotesOnDate(req: Request) { return { notes: editedNotes, - limit: limit, + limit: sqlParams.limit, } satisfies EditedNotesResponse; } From 942fbe033abc448b12b760199dacc5688eea1e4e Mon Sep 17 00:00:00 2001 From: contributor Date: Mon, 17 Nov 2025 12:42:05 +0200 Subject: [PATCH 19/19] edited notes: allow spaces between keyword and delta in #dateNote to be compatible with existing search syntax --- apps/server/src/routes/api/edited-notes.spec.ts | 12 +++++++++--- apps/server/src/routes/api/edited-notes.ts | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts index 7227a4c80b..2626af8bcf 100644 --- a/apps/server/src/routes/api/edited-notes.spec.ts +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -19,7 +19,7 @@ const expectedYear = "2025"; const expectedYearMinus1 = "2024"; function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) { - cls.init(() => { + cls.init(() => { cls.set("localNowDateTime", clientDate); const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword); expect(dateFilter.date).toBe(expectedDate); @@ -27,7 +27,7 @@ function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) { } function keywordDoesNotResolve(dateStrOrKeyword: string) { - cls.init(() => { + cls.init(() => { cls.set("localNowDateTime", clientDate); const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword); expect(dateFilter.date).toBe(null); @@ -36,7 +36,7 @@ function keywordDoesNotResolve(dateStrOrKeyword: string) { describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { beforeEach(() => { - vi.stubEnv('TZ', 'UTC'); + vi.stubEnv("TZ", "UTC"); vi.useFakeTimers(); vi.setSystemTime(new Date(serverDate)); }); @@ -67,6 +67,12 @@ describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => { keywordResolvesToDate("TODAY-1", expectedTodayMinus1); }); + it("resolves 'TODAY - 1' (with spaces) to yesterday's date", () => { + keywordResolvesToDate("TODAY - 1", expectedTodayMinus1); + keywordResolvesToDate("TODAY- 1", expectedTodayMinus1); + keywordResolvesToDate("TODAY -1", expectedTodayMinus1); + }); + it("resolves 'MONTH-2' to 2 months ago", () => { keywordResolvesToDate("MONTH-2", expectedMonthMinus2); }); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 262db76cbf..884b554c07 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -155,11 +155,11 @@ type DateFilter = DateValue; * @returns A `DateFilter` object containing the resolved date string. */ export function dateNoteLabelKeywordToDateFilter(dateStr: string): DateFilter { - const keywordAndDelta = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); + const keywordAndDelta = dateStr.match(/^(today|month|year)\s*([+-]\s*\d+)?$/i); if (keywordAndDelta) { const keyword = keywordAndDelta[1].toLowerCase(); - const delta = keywordAndDelta[2] ? parseInt(keywordAndDelta[2]) : 0; + const delta = parseInt(keywordAndDelta[2]?.replace(/\s/g, "") ?? "0"); const clientDate = dayjs(dateUtils.localNowDate()); const date = formatDateFromKeywordAndDelta(clientDate, keyword, delta);