-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
edited notes: recognize TODAY keyword #7704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
contributor
wants to merge
19
commits into
TriliumNext:main
Choose a base branch
from
contributor:edited-notes-recognize-keywords
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+339
−87
Draft
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
e6ebfc4
edited notes: move edited-notes related code to own module
contributor dd7c556
edited notes: recognize dateNote label value TODAY, MONTH, YEAR
contributor 8e989cf
edited notes: handle timezone differences between client and server
contributor 8e09ed8
edited notes: force tests to run in UTC timezone
contributor 674528b
edited notes: better docstring for resolveDateParams
contributor 5557cd4
lint format
contributor 3379a8f
edited notes: extract sqlquery var for eslint to recognize indentation
contributor d167caa
edited notes: move formatMap on module level
contributor 7aff5d1
edited notes: extendable EditedNotesResponse
contributor c389a12
edited notes: return limit in response
contributor 3ba6d14
edited notes: add happy path tests
contributor b54411a
edited notes: more descriptive name dateNoteLabelKeywordToDateFilter
contributor ad9a058
edited notes: better names in tests
contributor bcff929
edited notes: recognize not valid dates/keywords, return 0 limit to c…
contributor 25844d6
edited notes: add positive delta test
contributor 405fe0e
edited notes: more restrictive check
contributor cbdca3c
edited notes: do not shadow variable
contributor 2486d97
edited notes: use parameterized limit
contributor 942fbe0
edited notes: allow spaces between keyword and delta in #dateNote
contributor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import cls from '../../services/cls.js'; | ||
| import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; | ||
| import { dateNoteLabelKeywordToDateFilter } from "./edited-notes.js"; | ||
|
|
||
| // 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 expectedTodayPlus1 = "2025-01-02"; | ||
| const expectedMonth = "2025-01"; | ||
| const expectedMonthMinus2 = "2024-11"; | ||
| const expectedYear = "2025"; | ||
| const expectedYearMinus1 = "2024"; | ||
|
|
||
| function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) { | ||
| cls.init(() => { | ||
| cls.set("localNowDateTime", clientDate); | ||
| const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword); | ||
| expect(dateFilter.date).toBe(expectedDate); | ||
| }); | ||
| } | ||
|
|
||
| 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"); | ||
| vi.useFakeTimers(); | ||
| vi.setSystemTime(new Date(serverDate)); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| // Restore real timers after each test | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it("resolves 'TODAY' to today's date", () => { | ||
| keywordResolvesToDate("TODAY", expectedToday); | ||
| }); | ||
|
|
||
| it("resolves 'TODAY+1' to tomorrow's date", () => { | ||
| keywordResolvesToDate("TODAY+1", expectedTodayPlus1); | ||
| }); | ||
|
|
||
| it("resolves 'MONTH' to current month", () => { | ||
| keywordResolvesToDate("MONTH", expectedMonth); | ||
| }); | ||
|
|
||
| it("resolves 'YEAR' to current year", () => { | ||
| keywordResolvesToDate("YEAR", expectedYear); | ||
| }); | ||
|
|
||
| it("resolves 'TODAY-1' to yesterday's date", () => { | ||
| 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); | ||
| }); | ||
|
|
||
| it("resolves 'YEAR-1' to last year", () => { | ||
| keywordResolvesToDate("YEAR-1", expectedYearMinus1); | ||
| }); | ||
|
|
||
| it("returns original string for day", () => { | ||
| keywordResolvesToDate("2020-12-31", "2020-12-31"); | ||
| }); | ||
|
|
||
| it("returns original string for month", () => { | ||
| 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 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 null for missing delta", () => { | ||
| keywordDoesNotResolve("TODAY-"); | ||
| }); | ||
|
|
||
| it("resolves 'today' (lowercase) to today's date", () => { | ||
| keywordResolvesToDate("today", expectedToday); | ||
| }); | ||
|
|
||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| import dayjs from "dayjs"; | ||
| 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 { EditedNotes, EditedNotesResponse } from "@triliumnext/commons"; | ||
| import dateUtils from "../../services/date_utils.js"; | ||
|
|
||
| interface NotePath { | ||
| noteId: string; | ||
| branchId?: string; | ||
| title: string; | ||
| notePath: string[]; | ||
| path: string; | ||
| } | ||
|
|
||
| interface NotePojoWithNotePath extends NotePojo { | ||
| notePath?: string[] | null; | ||
| } | ||
|
|
||
| function getEditedNotesOnDate(req: Request) { | ||
| const dateFilter = dateNoteLabelKeywordToDateFilter(req.params.date); | ||
|
|
||
| if (!dateFilter.date) { | ||
| return { | ||
| notes: [], | ||
| limit: 0, | ||
| } satisfies EditedNotesResponse; | ||
| } | ||
|
|
||
| const sqlParams = { | ||
| date: dateFilter.date + "%", | ||
| limit: 50, | ||
| }; | ||
| const sqlQuery = /*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 :limit`; | ||
|
|
||
| const noteIds = sql.getColumn<string>( | ||
| sqlQuery, | ||
| sqlParams | ||
| ); | ||
|
|
||
| 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)); | ||
| } | ||
|
|
||
| const editedNotes = notes.map((note) => { | ||
| const notePath = getNotePathData(note); | ||
|
|
||
| const notePojo: NotePojoWithNotePath = note.getPojo(); | ||
| notePojo.notePath = notePath ? notePath.notePath : null; | ||
|
|
||
| return notePojo; | ||
| }); | ||
|
|
||
| return { | ||
| notes: editedNotes, | ||
| limit: sqlParams.limit, | ||
| } 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; | ||
eliandoran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return { | ||
| noteId: note.noteId, | ||
| branchId: branchId, | ||
| title: noteTitle, | ||
| notePath: retPath, | ||
| path: retPath.join("/") | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| const formatMap = new Map<string, { format: string, addUnit: dayjs.UnitType }>([ | ||
| ["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 handler = formatMap.get(keyword); | ||
|
|
||
| if (!handler) { | ||
| throw new Error(`Unrecognized keyword: ${keyword}`); | ||
| } | ||
|
|
||
| const date = startingDate.add(delta, handler.addUnit); | ||
| return date.format(handler.format); | ||
| } | ||
|
|
||
| interface DateValue { | ||
| // kind: "date", | ||
contributor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| date: string | null, | ||
| } | ||
|
|
||
| type DateFilter = DateValue; | ||
|
|
||
| /** | ||
| * 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 dateNoteLabelKeywordToDateFilter(dateStr: string): DateFilter { | ||
| const keywordAndDelta = dateStr.match(/^(today|month|year)\s*([+-]\s*\d+)?$/i); | ||
|
|
||
| if (keywordAndDelta) { | ||
| const keyword = keywordAndDelta[1].toLowerCase(); | ||
| const delta = parseInt(keywordAndDelta[2]?.replace(/\s/g, "") ?? "0"); | ||
|
|
||
| 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 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function isValidDatePrefix(dateStr: string): boolean { | ||
| // 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; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| export default { | ||
| getEditedNotesOnDate, | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.