diff --git a/bin/tpl/anonymize-database.sql b/bin/tpl/anonymize-database.sql index bc2b431d6..6309d5236 100644 --- a/bin/tpl/anonymize-database.sql +++ b/bin/tpl/anonymize-database.sql @@ -14,7 +14,8 @@ UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', - 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', + 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'weekPattern', 'enableWeekNote', 'monthPattern', + 'quarterPattern', 'yearPattern', 'enableQuarterNote', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', @@ -31,7 +32,8 @@ UPDATE attributes SET name = 'name' WHERE type = 'relation' 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', - 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', + 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'weekPattern', 'enableWeekNote', 'monthPattern', + 'quarterPattern', 'yearPattern', 'enableQuarterNote', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json index b6647c8bd..0ea953d4c 100644 --- a/docs/User Guide/!!!meta.json +++ b/docs/User Guide/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.92.6", + "appVersion": "0.92.7", "files": [ { "isClone": false, @@ -3511,58 +3511,51 @@ "position": 20 }, { - "type": "label", - "name": "shareAlias", - "value": "search", - "isInheritable": false, - "position": 20 - }, - { - "type": "label", - "name": "iconClass", - "value": "bx bx-search-alt-2", + "type": "relation", + "name": "internalLink", + "value": "OR8WJ7Iz9K4U", "isInheritable": false, "position": 30 }, { "type": "relation", "name": "internalLink", - "value": "xYmIYSP6wE3F", + "value": "wX4HbRucYSDD", "isInheritable": false, "position": 40 }, { "type": "relation", "name": "internalLink", - "value": "YtSN43OrfzaA", + "value": "ivYnonVFBxbQ", "isInheritable": false, "position": 50 }, { "type": "relation", "name": "internalLink", - "value": "OR8WJ7Iz9K4U", + "value": "xYmIYSP6wE3F", "isInheritable": false, "position": 60 }, { "type": "relation", "name": "internalLink", - "value": "9sRHySam5fXb", + "value": "YtSN43OrfzaA", "isInheritable": false, "position": 70 }, { "type": "relation", "name": "internalLink", - "value": "m523cpzocqaD", + "value": "9sRHySam5fXb", "isInheritable": false, "position": 80 }, { "type": "relation", "name": "internalLink", - "value": "wX4HbRucYSDD", + "value": "m523cpzocqaD", "isInheritable": false, "position": 90 }, @@ -3590,16 +3583,23 @@ { "type": "relation", "name": "internalLink", - "value": "ivYnonVFBxbQ", + "value": "oPVyFC7WL2Lp", "isInheritable": false, "position": 130 }, { - "type": "relation", - "name": "internalLink", - "value": "oPVyFC7WL2Lp", + "type": "label", + "name": "shareAlias", + "value": "search", "isInheritable": false, - "position": 140 + "position": 20 + }, + { + "type": "label", + "name": "iconClass", + "value": "bx bx-search-alt-2", + "isInheritable": false, + "position": 30 } ], "format": "markdown", @@ -3852,16 +3852,16 @@ "mime": "text/html", "attributes": [ { - "type": "label", - "name": "iconClass", - "value": "bx bx-search-alt-2", + "type": "relation", + "name": "internalLink", + "value": "MI26XDLSAlCD", "isInheritable": false, "position": 10 }, { "type": "relation", "name": "internalLink", - "value": "8YBEPzcpUgxw", + "value": "iPIMuisry3hd", "isInheritable": false, "position": 20 }, @@ -3875,16 +3875,16 @@ { "type": "relation", "name": "internalLink", - "value": "iPIMuisry3hd", + "value": "8YBEPzcpUgxw", "isInheritable": false, "position": 40 }, { - "type": "relation", - "name": "internalLink", - "value": "MI26XDLSAlCD", + "type": "label", + "name": "iconClass", + "value": "bx bx-search-alt-2", "isInheritable": false, - "position": 50 + "position": 10 } ], "format": "markdown", diff --git a/docs/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.md b/docs/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.md index 9b48429a9..06e40a9d0 100644 --- a/docs/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.md +++ b/docs/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.md @@ -13,10 +13,14 @@ This pattern works well also because of [Cloning Notes](../../Basic%20Concepts%2 ![](Day%20Notes_image.png) -You can see the structure of day notes appearing under "Journal" note - there's a note for the whole year 2017, under it, you have "12 - December" which then contains "18 - Monday". This is our "day note" which contains some text in its content and also has some child notes (some of them are from [Task manager](Task%20Manager.md)). +You can see the structure of day notes appearing under "Journal" note - there's a note for the whole year 2025, under it, you have "03 - March" which then contains "09 - Monday". This is our "day note" which contains some text in its content and also has some child notes (some of them are from [Task manager](Task%20Manager.md)). You can also notice how this day note has [promoted attribute](../Attributes/Promoted%20Attributes.md) "weight" where you can track your daily weight. This data is then used in [Weight tracker](Weight%20Tracker.md). +## Week Note and Quarter Note + +Week and quarter notes are disabled by default, since it might be too much for some people. To enable them, you need to set `#enableWeekNotes` and `#enableQuarterNotes` attributes on the root calendar note, which is identified by `#calendarRoot` label. Week note is affected by the first week of year option. Be careful when you already have some week notes created, it will not automatically change the existing week notes and might lead to some duplicates. + ## Templates Trilium provides [template](../Templates.md) functionality, and it could be used together with day notes. @@ -24,26 +28,48 @@ Trilium provides [template](../Templates.md) functionality, and it could be used You can define one of the following relations on the root of the journal (identified by `#calendarRoot` label): * yearTemplate +* quarterTemplate (if `#enableQuarterNotes` is set) * monthTemplate +* weekTemplate (if `#enableWeekNotes` is set) * dateTemplate All of these are relations. When Trilium creates a new note for year or month or date, it will take a look at the root and attach a corresponding `~template` relation to the newly created role. Using this, you can e.g. create your daily template with e.g. checkboxes for daily routine etc. -## Date pattern +## Naming pattern -It's possible to customize the title of generated date notes by defining a `#datePattern` label on a root calendar note (identified by `#calendarRoot` label). Following are possible values: +You can customize the title of generated journal notes by defining a `#datePattern`, `#weekPattern`, `#monthPattern`, `#quarterPattern` and `#yearPattern` attribute on a root calendar note (identified by `#calendarRoot` label). The naming pattern replacements follow a level-up compatibility - each level can use replacements from itself and all levels above it. For example, `#monthPattern` can use month, quarter and year replacements, while `#weekPattern` can use week, month, quarter and year replacements. But it is not possible to use week replacements in `#monthPattern`. -* `{dayInMonthPadded} - {weekDay}` day notes are named e.g. "24 - Monday" -* `{dayInMonthPadded}: {weekDay3}` day notes are named e.g. "24: Mon" -* `{dayInMonthPadded}: {weekDay2}` day notes are named e.g. "24: Mo" -* `{isoDate} - {weekDay}` day notes are named e.g. "2020-12-24 - Monday" +### Date pattern + +It's possible to customize the title of generated date notes by defining a `#datePattern` attribute on a root calendar note (identified by `#calendarRoot` label). Following are possible values: + +* `{isoDate}` results in an ISO 8061 formatted date (e.g. "2025-03-09" for March 9, 2025) +* `{dateNumber}` results in a number like `9` for the 9th day of the month, `11` for the 11th day of the month +* `{dateNumberPadded}` results in a number like `09` for the 9th day of the month, `11` for the 11th day of the month * `{ordinal}` is replaced with the ordinal date (e.g. 1st, 2nd, 3rd) etc. +* `{weekDay}` results in the full day name (e.g. `Monday`) +* `{weekDay3}` is replaced with the first 3 letters of the day, e.g. Mon, Tue, etc. +* `{weekDay2}` is replaced with the first 2 letters of the day, e.g. Mo, Tu, etc. -## Month pattern +The default is `{dateNumberPadded} - {weekDay}` -It is also possible to customize the title of generated month notes through the `#monthPattern` attribute, much like `#datePattern`. The options are: +### Week pattern + +It is also possible to customize the title of generated week notes through the `#weekPattern` attribute on the root calendar note. The options are: + +* `{weekNumber}` results in a number like `9` for the 9th week of the year, `11` for the 11th week of the year +* `{weekNumberPadded}` results in a number like `09` for the 9th week of the year, `11` for the 11th week of the year +* `{shortWeek}` results in a short week string like `W9` for the 9th week of the year, `W11` for the 11th week of the year +* `{shortWeek3}` results in a short week string like `W09` for the 9th week of the year, `W11` for the 11th week of the year + +The default is `Week {weekNumber}` + +### Month pattern + +It is also possible to customize the title of generated month notes through the `#monthPattern` attribute on the root calendar note. The options are: * `{isoMonth}` results in an ISO 8061 formatted month (e.g. "2025-03" for March 2025) +* `{monthNumber}` results in a number like `9` for September, and `11` for November * `{monthNumberPadded}` results in a number like `09` for September, and `11` for November * `{month}` results in the full month name (e.g. `September` or `October`) * `{shortMonth3}` is replaced with the first 3 letters of the month, e.g. Jan, Feb, etc. @@ -51,10 +77,27 @@ It is also possible to customize the title of generated month notes through the The default is `{monthNumberPadded} - {month}` +### Quarter pattern + +It is also possible to customize the title of generated quarter notes through the `#quarterPattern` attribute on the root calendar note. The options are: + +* `{quarterNumber}` results in a number like `1` for the 1st quarter of the year +* `{shortQuarter}` results in a short quarter string like `Q1` for the 1st quarter of the year + +The default is `Quarter {quarterNumber}` + +### Year pattern + +It is also possible to customize the title of generated year notes through the `#yearPattern` attribute on the root calendar note. The options are: + +* `{year}` results in the full year (e.g. `2025`) + +The default is `{year}` + ## Implementation Trilium has some special support for day notes in the form of [backend Script API](https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html) - see e.g. getDayNote() function. -Day (and year, month) notes are created with a label - e.g. `#dateNote="2018-08-16"` this can then be used by other scripts to add new notes to day note etc. +Day (and year, month) notes are created with a label - e.g. `#dateNote="2025-03-09"` this can then be used by other scripts to add new notes to day note etc. -Journal also has relation `child:child:child:template=Day template` (see \[\[attribute inheritance\]\]) which effectively adds \[\[template\]\] to day notes (grand-grand-grand children of Journal). \ No newline at end of file +Journal also has relation `child:child:child:template=Day template` (see \[\[attribute inheritance\]\]) which effectively adds \[\[template\]\] to day notes (grand-grand-grand children of Journal). Please note that, when you enable week notes or quarter notes, it will not automatically change the relation for the child level. \ No newline at end of file diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index cc2b9f1a1..6114b21a1 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -4680,7 +4680,7 @@ otherwise (by e.g. createLink()) -

getWeekNote(date) → {Promise.<FNote>}

+

getWeekFirstDayNote(date) → {Promise.<FNote>}

diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 53cd61c4c..bee5628fa 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -563,7 +563,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @param {string} date - e.g. "2019-04-29" * @returns {Promise<FNote>} */ - this.getWeekNote = dateNotesService.getWeekNote; + this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote; /** * Returns month-note. If it doesn't exist, it is automatically created. diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index eed39fd80..ac2f01f55 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -694,7 +694,7 @@ paths: /calendar/weeks/{date}: get: description: returns a week note for a given date. Gets created if doesn't exist. - operationId: getWeekNote + operationId: getWeekFirstDayNote parameters: - name: date in: path diff --git a/src/etapi/special_notes.ts b/src/etapi/special_notes.ts index 23411c987..df2f75f9a 100644 --- a/src/etapi/special_notes.ts +++ b/src/etapi/special_notes.ts @@ -5,59 +5,72 @@ import mappers from "./mappers.js"; import type { Router } from "express"; const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); +const getWeekInvalidError = (week: string) => new eu.EtapiError(400, "WEEK_INVALID", `Week "${week}" is not valid.`); +const getWeekNotFoundError = (week: string) => new eu.EtapiError(404, "WEEK_NOT_FOUND", `Week "${week}" not found. Check if week note is enabled.`); const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`); function isValidDate(date: string) { - if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) { - return false; - } - - return !!Date.parse(date); + return /[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date) && !!Date.parse(date); } function register(router: Router) { - eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => { + eu.route(router, "get", "/etapi/inbox/:date", async (req, res, next) => { + const { date } = req.params; + + if (!isValidDate(date)) { + throw getDateInvalidError(date); + } + const note = await specialNotesService.getInboxNote(date); + res.json(mappers.mapNoteToPojo(note)); + }); + + eu.route(router, "get", "/etapi/calendar/days/:date", async (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { throw getDateInvalidError(date); } - const note = specialNotesService.getInboxNote(date); + const note = await dateNotesService.getDayNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/week-first-day/:date", async (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { throw getDateInvalidError(date); } - const note = dateNotesService.getDayNote(date); + const note = await dateNotesService.getWeekFirstDayNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/weeks/:date", (req, res, next) => { - const { date } = req.params; + eu.route(router, "get", "/etapi/calendar/weeks/:week", async (req, res, next) => { + const { week } = req.params; - if (!isValidDate(date)) { - throw getDateInvalidError(date); + if (!/[0-9]{4}-W[0-9]{2}/.test(week)) { + throw getWeekInvalidError(week); + } + + const note = await dateNotesService.getWeekNote(week); + + if (!note) { + throw getWeekNotFoundError(week); } - const note = dateNotesService.getWeekNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/months/:month", async (req, res, next) => { const { month } = req.params; if (!/[0-9]{4}-[0-9]{2}/.test(month)) { throw getMonthInvalidError(month); } - const note = dateNotesService.getMonthNote(month); + const note = await dateNotesService.getMonthNote(month); res.json(mappers.mapNoteToPojo(note)); }); diff --git a/src/public/app/doc_notes/en/User Guide/!!!meta.json b/src/public/app/doc_notes/en/User Guide/!!!meta.json index 1fdde0cea..506526e7b 100644 --- a/src/public/app/doc_notes/en/User Guide/!!!meta.json +++ b/src/public/app/doc_notes/en/User Guide/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.92.6", + "appVersion": "0.92.7", "files": [ { "isClone": false, @@ -3511,58 +3511,51 @@ "position": 20 }, { - "type": "label", - "name": "shareAlias", - "value": "search", - "isInheritable": false, - "position": 20 - }, - { - "type": "label", - "name": "iconClass", - "value": "bx bx-search-alt-2", + "type": "relation", + "name": "internalLink", + "value": "OR8WJ7Iz9K4U", "isInheritable": false, "position": 30 }, { "type": "relation", "name": "internalLink", - "value": "xYmIYSP6wE3F", + "value": "wX4HbRucYSDD", "isInheritable": false, "position": 40 }, { "type": "relation", "name": "internalLink", - "value": "YtSN43OrfzaA", + "value": "ivYnonVFBxbQ", "isInheritable": false, "position": 50 }, { "type": "relation", "name": "internalLink", - "value": "OR8WJ7Iz9K4U", + "value": "xYmIYSP6wE3F", "isInheritable": false, "position": 60 }, { "type": "relation", "name": "internalLink", - "value": "9sRHySam5fXb", + "value": "YtSN43OrfzaA", "isInheritable": false, "position": 70 }, { "type": "relation", "name": "internalLink", - "value": "m523cpzocqaD", + "value": "9sRHySam5fXb", "isInheritable": false, "position": 80 }, { "type": "relation", "name": "internalLink", - "value": "wX4HbRucYSDD", + "value": "m523cpzocqaD", "isInheritable": false, "position": 90 }, @@ -3590,16 +3583,23 @@ { "type": "relation", "name": "internalLink", - "value": "ivYnonVFBxbQ", + "value": "oPVyFC7WL2Lp", "isInheritable": false, "position": 130 }, { - "type": "relation", - "name": "internalLink", - "value": "oPVyFC7WL2Lp", + "type": "label", + "name": "shareAlias", + "value": "search", "isInheritable": false, - "position": 140 + "position": 20 + }, + { + "type": "label", + "name": "iconClass", + "value": "bx bx-search-alt-2", + "isInheritable": false, + "position": 30 } ], "format": "html", @@ -3852,16 +3852,16 @@ "mime": "text/html", "attributes": [ { - "type": "label", - "name": "iconClass", - "value": "bx bx-search-alt-2", + "type": "relation", + "name": "internalLink", + "value": "MI26XDLSAlCD", "isInheritable": false, "position": 10 }, { "type": "relation", "name": "internalLink", - "value": "8YBEPzcpUgxw", + "value": "iPIMuisry3hd", "isInheritable": false, "position": 20 }, @@ -3875,16 +3875,16 @@ { "type": "relation", "name": "internalLink", - "value": "iPIMuisry3hd", + "value": "8YBEPzcpUgxw", "isInheritable": false, "position": 40 }, { - "type": "relation", - "name": "internalLink", - "value": "MI26XDLSAlCD", + "type": "label", + "name": "iconClass", + "value": "bx bx-search-alt-2", "isInheritable": false, - "position": 50 + "position": 10 } ], "format": "html", diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html index 935e6d0d7..621717007 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html @@ -35,12 +35,19 @@

You can see the structure of day notes appearing under "Journal" note - - there's a note for the whole year 2017, under it, you have "12 - December" - which then contains "18 - Monday". This is our "day note" which contains + - there's a note for the whole year 2025, under it, you have "03 - March" + which then contains "09 - Monday". This is our "day note" which contains some text in its content and also has some child notes (some of them are from Task manager).

You can also notice how this day note has promoted attribute "weight" where you can track your daily weight. This data is then used in Weight tracker.

+

Week Note and Quarter Note

+

Week and quarter notes are disabled by default, since it might be too + much for some people. To enable them, you need to set #enableWeekNotes and #enableQuarterNotes attributes + on the root calendar note, which is identified by #calendarRoot label. + Week note is affected by the first week of year option. Be careful when + you already have some week notes created, it will not automatically change + the existing week notes and might lead to some duplicates.

Templates

Trilium provides template functionality, and it could be used together with day notes.

@@ -48,36 +55,69 @@ (identified by #calendarRoot label):

All of these are relations. When Trilium creates a new note for year or month or date, it will take a look at the root and attach a corresponding ~template relation to the newly created role. Using this, you can e.g. create your daily template with e.g. checkboxes for daily routine etc.

-

Date pattern

+

Naming pattern

+

You can customize the title of generated journal notes by defining a #datePattern, #weekPattern, #monthPattern, #quarterPattern and #yearPattern attribute + on a root calendar note (identified by #calendarRoot label). + The naming pattern replacements follow a level-up compatibility - each + level can use replacements from itself and all levels above it. For example, #monthPattern can + use month, quarter and year replacements, while #weekPattern can + use week, month, quarter and year replacements. But it is not possible + to use week replacements in #monthPattern.

+

Date pattern

It's possible to customize the title of generated date notes by defining - a #datePattern label on a root calendar note (identified by #calendarRoot label). - Following are possible values:

+ a #datePattern attribute on a root calendar note (identified + by #calendarRoot label). Following are possible values:

-

Month pattern

+

The default is {dateNumberPadded} - {weekDay} +

+

Week pattern

+

It is also possible to customize the title of generated week notes through + the #weekPattern attribute on the root calendar note. The options + are:

+ +

The default is Week {weekNumber} +

+

Month pattern

It is also possible to customize the title of generated month notes through - the #monthPattern attribute, much like #datePattern. - The options are:

+ the #monthPattern attribute on the root calendar note. The options + are:

The default is {monthNumberPadded} - {month}

+

Quarter pattern

+

It is also possible to customize the title of generated quarter notes + through the #quarterPattern attribute on the root calendar note. + The options are:

+ +

The default is Quarter {quarterNumber} +

+

Year pattern

+

It is also possible to customize the title of generated year notes through + the #yearPattern attribute on the root calendar note. The options + are:

+ +

The default is {year} +

Implementation

Trilium has some special support for day notes in the form of backend Script API - see e.g. getDayNote() function.

-

Day (and year, month) notes are created with a label - e.g. #dateNote="2018-08-16" this +

Day (and year, month) notes are created with a label - e.g. #dateNote="2025-03-09" this can then be used by other scripts to add new notes to day note etc.

Journal also has relation child:child:child:template=Day template (see [[attribute inheritance]]) which effectively adds [[template]] to day notes - (grand-grand-grand children of Journal).

+ (grand-grand-grand children of Journal). Please note that, when you enable + week notes or quarter notes, it will not automatically change the relation + for the child level.

diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note.html b/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note.html index 5ad1117bd..e7c176de2 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note.html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note.html @@ -21,19 +21,18 @@

Alternatives

Accessing the search

Interaction

diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search.html b/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search.html index 8d02358af..85c05ea0c 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search.html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Navigation/Search.html @@ -24,10 +24,10 @@ results as sub-items.

Accessing the search

Interaction

@@ -43,8 +43,8 @@
  • To limit the search to a note and its sub-children, set a note in Ancestor.
      -
    1. This value is also pre-filled if the search is triggered from a hoisted note or - a workspace.
    2. +
    3. This value is also pre-filled if the search is triggered from a hoisted note or + a workspace.
    4. To search the entire database, keep the value empty.
  • @@ -58,7 +58,7 @@
  • The Search & Execute actions button is only relevant if at least one action has been added (as described in the section below).
  • The Save to note will create a new note with the search configuration. - For more information, see Saved Search.
  • + For more information, see Saved Search.

    Search options

    Click on which search option to apply from the Add search option section.

    @@ -71,7 +71,7 @@
    1. Search script
        -
      1. This feature allows writing a Code note +
      2. This feature allows writing a Code note that will handle the search on its own.
    2. @@ -79,12 +79,12 @@
      1. The search will not look into the content of the notes, but it will still look into note titles and attributes, relations (based on the search query).
      2. -
      3. This method can speed up the search considerably for large databases.
      4. +
      5. This method can speed up the search considerably for large databases.
    3. Include archived
        -
      1. Archived Notes will +
      2. Archived Notes will also be included in the results, whereas otherwise they would be ignored.
    4. @@ -107,7 +107,7 @@
      1. This will print additional information in the server log (see  Error logs), regarding how the search expression was parsed.
      2. + class="reference-link" href="#root/_help_qzNzp9LYQyPT">Error logs), regarding how the search expression was parsed.
      3. This function is especially useful after understanding the search functionality in detail, in order to determine why a complex search query is not working as expected.
      4. @@ -122,10 +122,9 @@ action multiple times (i.e. in order to be able to apply multiple labels to notes).
      5. The actions given are the same as the ones in Bulk Actions, - which is an alternative for operating directly with notes within the  - Note Tree.
      6. + href="#root/_help_ivYnonVFBxbQ">Bulk Actions, which is an alternative + for operating directly with notes within the Note Tree.
      7. After defining the actions, first press Search to check the matched notes and then press Search & Execute actions to trigger the actions.
      8. diff --git a/src/public/app/services/date_notes.ts b/src/public/app/services/date_notes.ts index 17aa7b963..51d8e68a2 100644 --- a/src/public/app/services/date_notes.ts +++ b/src/public/app/services/date_notes.ts @@ -22,14 +22,22 @@ async function getDayNote(date: string) { return await froca.getNote(note.noteId); } -async function getWeekNote(date: string) { - const note = await server.get(`special-notes/weeks/${date}`, "date-note"); +async function getWeekFirstDayNote(date: string) { + const note = await server.get(`special-notes/week-first-day/${date}`, "date-note"); await ws.waitForMaxKnownEntityChangeId(); return await froca.getNote(note.noteId); } +async function getWeekNote(week: string) { + const note = await server.get(`special-notes/weeks/${week}`, "date-note"); + + await ws.waitForMaxKnownEntityChangeId(); + + return await froca.getNote(note?.noteId); +} + async function getMonthNote(month: string) { const note = await server.get(`special-notes/months/${month}`, "date-note"); @@ -38,6 +46,14 @@ async function getMonthNote(month: string) { return await froca.getNote(note.noteId); } +async function getQuarterNote(quarter: string) { + const note = await server.get(`special-notes/quarters/${quarter}`, "date-note"); + + await ws.waitForMaxKnownEntityChangeId(); + + return await froca.getNote(note.noteId); +} + async function getYearNote(year: string) { const note = await server.get(`special-notes/years/${year}`, "date-note"); @@ -66,7 +82,9 @@ export default { getInboxNote, getTodayNote, getDayNote, + getWeekFirstDayNote, getWeekNote, + getQuarterNote, getMonthNote, getYearNote, createSqlConsole, diff --git a/src/public/app/services/frontend_script_api.ts b/src/public/app/services/frontend_script_api.ts index bd30a58cc..cfcf076cd 100644 --- a/src/public/app/services/frontend_script_api.ts +++ b/src/public/app/services/frontend_script_api.ts @@ -363,6 +363,14 @@ interface Api { * * @param date - e.g. "2019-04-29" */ + getWeekFirstDayNote: typeof dateNotesService.getWeekFirstDayNote; + + /** + * Returns week note for given date. If such a note doesn't exist, it is automatically created. + * + * @param date in YYYY-MM-DD format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ getWeekNote: typeof dateNotesService.getWeekNote; /** @@ -372,6 +380,14 @@ interface Api { */ getMonthNote: typeof dateNotesService.getMonthNote; + /** + * Returns quarter note for given date. If such a note doesn't exist, it is automatically created. + * + * @param date in YYYY-MM format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getQuarterNote: typeof dateNotesService.getQuarterNote; + /** * Returns year-note. If it doesn't exist, it is automatically created. * @@ -651,8 +667,10 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig this.getTodayNote = dateNotesService.getTodayNote; this.getDayNote = dateNotesService.getDayNote; + this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote; this.getWeekNote = dateNotesService.getWeekNote; this.getMonthNote = dateNotesService.getMonthNote; + this.getQuarterNote = dateNotesService.getQuarterNote; this.getYearNote = dateNotesService.getYearNote; this.setHoistedNoteId = (noteId) => { diff --git a/src/public/app/widgets/buttons/calendar.ts b/src/public/app/widgets/buttons/calendar.ts index 11e9092bc..a8b2d4542 100644 --- a/src/public/app/widgets/buttons/calendar.ts +++ b/src/public/app/widgets/buttons/calendar.ts @@ -1,5 +1,4 @@ import { t } from "../../services/i18n.js"; -import utils from "../../services/utils.js"; import dateNoteService from "../../services/date_notes.js"; import server from "../../services/server.js"; import appContext from "../../components/app_context.js"; @@ -8,8 +7,15 @@ import toastService from "../../services/toast.js"; import options from "../../services/options.js"; import { Dropdown } from "bootstrap"; import type { EventData } from "../../components/app_context.js"; +import dayjs, { Dayjs } from "dayjs"; +import utc from "dayjs/plugin/utc.js"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js"; +import type BAttribute from "../../../../becca/entities/battribute.js"; import "../../../stylesheets/calendar.css"; +dayjs.extend(utc); +dayjs.extend(isSameOrAfter); + const MONTHS = [ t("calendar.january"), t("calendar.febuary"), @@ -29,7 +35,7 @@ const DROPDOWN_TPL = `
        @@ -59,9 +65,7 @@ const DROPDOWN_TPL = `
        -
        -
        - +
        `; @@ -71,6 +75,11 @@ interface DateNotesForMonth { [date: string]: string; } +interface WeekCalculationOptions { + firstWeekType: number; + minDaysInFirstWeek: number; +} + export default class CalendarWidget extends RightDropdownButtonWidget { private $month!: JQuery; private $weekHeader!: JQuery; @@ -82,9 +91,12 @@ export default class CalendarWidget extends RightDropdownButtonWidget { private $previousYear!: JQuery; private monthDropdown!: Dropdown; private firstDayOfWeek!: number; - private activeDate: Date | null = null; - private todaysDate!: Date; - private date!: Date; + private weekCalculationOptions!: WeekCalculationOptions; + private activeDate: Dayjs | null = null; + private todaysDate!: Dayjs; + private date!: Dayjs; + private weekNoteEnable: boolean = false; + private weekNotes: string[] = []; constructor(title: string = "", icon: string = "") { super(title, icon, DROPDOWN_TPL); @@ -97,6 +109,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.$weekHeader = this.$dropdownContent.find(".calendar-week"); this.manageFirstDayOfWeek(); + this.initWeekCalculation(); // Month navigation this.$monthSelect = this.$dropdownContent.find('[data-calendar-input="month"]'); @@ -109,18 +122,18 @@ export default class CalendarWidget extends RightDropdownButtonWidget { const target = e.target as HTMLElement; const value = target.dataset.value; if (value) { - this.date.setMonth(parseInt(value)); + this.date = this.date.month(parseInt(value)); this.createMonth(); } }); this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]'); this.$next.on("click", () => { - this.date.setMonth(this.date.getMonth() + 1); + this.date = this.date.add(1, 'month'); this.createMonth(); }); this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]'); this.$previous.on("click", () => { - this.date.setMonth(this.date.getMonth() - 1); + this.date = this.date.subtract(1, 'month'); this.createMonth(); }); @@ -128,17 +141,17 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]'); this.$yearSelect.on("input", (e) => { const target = e.target as HTMLInputElement; - this.date.setFullYear(parseInt(target.value)); + this.date = this.date.year(parseInt(target.value)); this.createMonth(); }); this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]'); this.$nextYear.on("click", () => { - this.date.setFullYear(this.date.getFullYear() + 1); + this.date = this.date.add(1, 'year'); this.createMonth(); }); this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]'); this.$previousYear.on("click", () => { - this.date.setFullYear(this.date.getFullYear() - 1); + this.date = this.date.subtract(1, 'year'); this.createMonth(); }); @@ -159,6 +172,27 @@ export default class CalendarWidget extends RightDropdownButtonWidget { ev.stopPropagation(); }); + this.$dropdownContent.on("click", ".calendar-week-number", async (ev) => { + if (!this.weekNoteEnable) { + return; + } + + const week = $(ev.target).closest(".calendar-week-number").attr("data-calendar-week-number"); + + if (week) { + const note = await dateNoteService.getWeekNote(week); + + if (note) { + appContext.tabManager.getActiveContext()?.setNote(note.noteId); + this.dropdown?.hide(); + } else { + toastService.showError(t("calendar.cannot_find_week_note")); + } + } + + ev.stopPropagation(); + }); + // Handle click events for the entire calendar widget this.$dropdownContent.on("click", (e) => { const $target = $(e.target); @@ -177,57 +211,139 @@ export default class CalendarWidget extends RightDropdownButtonWidget { }); } + private async getWeekNoteEnable() { + const noteId = await server.get(`search/${encodeURIComponent('#calendarRoot')}`); + if (noteId.length === 0) { + this.weekNoteEnable = false; + return; + } + const noteAttributes = await server.get(`notes/${noteId}/attributes`); + + for (const attribute of noteAttributes) { + if (attribute.name === 'enableWeekNote') { + this.weekNoteEnable = true; + return + } + } + this.weekNoteEnable = false; + } + manageFirstDayOfWeek() { this.firstDayOfWeek = options.getInt("firstDayOfWeek") || 0; // Generate the list of days of the week taking into consideration the user's selected first day of week. let localeDaysOfWeek = [...DAYS_OF_WEEK]; const daysToBeAddedAtEnd = localeDaysOfWeek.splice(0, this.firstDayOfWeek); - localeDaysOfWeek = [...localeDaysOfWeek, ...daysToBeAddedAtEnd]; + localeDaysOfWeek = ['', ...localeDaysOfWeek, ...daysToBeAddedAtEnd]; this.$weekHeader.html(localeDaysOfWeek.map((el) => `${el}`).join('')); } + initWeekCalculation() { + this.weekCalculationOptions = { + firstWeekType: options.getInt("firstWeekOfYear") || 0, + minDaysInFirstWeek: options.getInt("minDaysInFirstWeek") || 4 + }; + } + + getWeekNumber(date: Dayjs): number { + const year = date.year(); + const dayOfWeek = (day: number) => (day - this.firstDayOfWeek + 7) % 7; + + // Get first day of the year and adjust to first week start + const jan1 = date.clone().year(year).month(0).date(1); + const jan1Weekday = jan1.day(); + const dayOffset = dayOfWeek(jan1Weekday); + let firstWeekStart = jan1.clone().subtract(dayOffset, 'day'); + + // Adjust based on week rule + switch (this.weekCalculationOptions.firstWeekType) { + case 1: { // ISO 8601: first week contains Thursday + const thursday = firstWeekStart.clone().add(3, 'day'); // Monday + 3 = Thursday + if (thursday.year() < year) { + firstWeekStart = firstWeekStart.add(7, 'day'); + } + break; + } + case 2: { // minDaysInFirstWeek rule + const daysInFirstWeek = 7 - dayOffset; + if (daysInFirstWeek < this.weekCalculationOptions.minDaysInFirstWeek) { + firstWeekStart = firstWeekStart.add(7, 'day'); + } + break; + } + // default case 0: week containing Jan 1 → already handled + } + + const diffDays = date.startOf('day').diff(firstWeekStart.startOf('day'), 'day'); + const weekNumber = Math.floor(diffDays / 7) + 1; + + // Handle case when date is before first week start → belongs to last week of previous year + if (weekNumber <= 0) { + return this.getWeekNumber(date.subtract(1, 'day')); + } + + // Handle case when date belongs to first week of next year + const nextYear = year + 1; + const jan1Next = date.clone().year(nextYear).month(0).date(1); + const jan1WeekdayNext = jan1Next.day(); + const offsetNext = dayOfWeek(jan1WeekdayNext); + let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, 'day'); + + switch (this.weekCalculationOptions.firstWeekType) { + case 1: { + const thursday = nextYearWeekStart.clone().add(3, 'day'); + if (thursday.year() < nextYear) { + nextYearWeekStart = nextYearWeekStart.add(7, 'day'); + } + break; + } + case 2: { + const daysInFirstWeek = 7 - offsetNext; + if (daysInFirstWeek < this.weekCalculationOptions.minDaysInFirstWeek) { + nextYearWeekStart = nextYearWeekStart.add(7, 'day'); + } + break; + } + } + + if (date.isSameOrAfter(nextYearWeekStart)) { + return 1; + } + + return weekNumber; + } + async dropdownShown() { + await this.getWeekNoteEnable(); + this.weekNotes = await server.get(`attribute-values/weekNote`); this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null); } init(activeDate: string | null) { // attaching time fixes local timezone handling - this.activeDate = activeDate ? new Date(`${activeDate}T12:00:00`) : null; - this.todaysDate = new Date(); - this.date = new Date((this.activeDate || this.todaysDate).getTime()); - this.date.setDate(1); + this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null; + this.todaysDate = dayjs(); + this.date = dayjs(this.activeDate || this.todaysDate).startOf('month'); this.createMonth(); } - createDay(dateNotesForMonth: DateNotesForMonth, num: number, day: number) { - const $newDay = $("").addClass("calendar-date").attr("data-calendar-date", utils.formatDateISO(this.date)); + createDay(dateNotesForMonth: DateNotesForMonth, num: number) { + const $newDay = $("").addClass("calendar-date").attr("data-calendar-date", this.date.local().format('YYYY-MM-DD')); const $date = $("").html(String(num)); - // if it's the first day of the month - if (num === 1) { - // 0 1 2 3 4 5 6 - // Su Mo Tu We Th Fr Sa - // 1 2 3 4 5 6 0 - // Mo Tu We Th Fr Sa Su - let dayOffset = day - this.firstDayOfWeek; - if (dayOffset < 0) dayOffset = 7 + dayOffset; - $newDay.css("marginLeft", dayOffset * 14.28 + "%"); - } - - const dateNoteId = dateNotesForMonth[utils.formatDateISO(this.date)]; + const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')]; if (dateNoteId) { $newDay.addClass("calendar-date-exists"); $newDay.attr("data-href", `#root/${dateNoteId}`); } - if (this.isEqual(this.date, this.activeDate)) { + if (this.date.isSame(this.activeDate, 'day')) { $newDay.addClass("calendar-date-active"); } - if (this.isEqual(this.date, this.todaysDate)) { + if (this.date.isSame(this.todaysDate, 'day')) { $newDay.addClass("calendar-date-today"); } @@ -235,44 +351,140 @@ export default class CalendarWidget extends RightDropdownButtonWidget { return $newDay; } - isEqual(a: Date, b: Date | null) { - if ((!a && b) || (a && !b)) { - return false; + createWeekNumber(weekNumber: number) { + const weekNoteId = this.date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0'); + + let $newWeekNumber; + if (this.weekNoteEnable) { + // Utilize the hover effect of calendar-date + $newWeekNumber = $("").addClass("calendar-date"); + + if (this.weekNotes.includes(weekNoteId)) { + $newWeekNumber.addClass("calendar-date-exists"); + $newWeekNumber.attr("data-href", `#root/${weekNoteId}`); + } + + } else { + $newWeekNumber = $("").addClass("calendar-week-number-disabled"); + } + $newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId); + $newWeekNumber.append($("").html(String(weekNumber))); + + return $newWeekNumber; + } + + private getPrevMonthDays(firstDayOfWeek: number): { weekNumber: number, dates: Dayjs[] } { + const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month'); + const daysToAdd = (firstDayOfWeek - this.firstDayOfWeek + 7) % 7; + const dates: Dayjs[] = []; + + const firstDay = this.date.startOf('month'); + const weekNumber = this.getWeekNumber(firstDay); + + // Get dates from previous month + for (let i = daysToAdd - 1; i >= 0; i--) { + dates.push(prevMonthLastDay.subtract(i, 'day')); } - if (!b) return false; + return { weekNumber, dates }; + } - return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); + private getNextMonthDays(lastDayOfWeek: number): Dayjs[] { + const nextMonthFirstDay = this.date.add(1, 'month').startOf('month'); + const dates: Dayjs[] = []; + + const lastDayOfUserWeek = (this.firstDayOfWeek + 6) % 7; + const daysToAdd = (lastDayOfUserWeek - lastDayOfWeek + 7) % 7; + + // Get dates from next month + for (let i = 0; i < daysToAdd; i++) { + dates.push(nextMonthFirstDay.add(i, 'day')); + } + + return dates; } async createMonth() { - const month = utils.formatDateISO(this.date).substr(0, 7); + const month = this.date.format('YYYY-MM'); const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`); this.$month.empty(); - const currentMonth = this.date.getMonth(); - while (this.date.getMonth() === currentMonth) { - const $day = this.createDay(dateNotesForMonth, this.date.getDate(), this.date.getDay()); + const firstDay = this.date.startOf('month'); + const firstDayOfWeek = firstDay.day(); + // Add dates from previous month + if (firstDayOfWeek !== this.firstDayOfWeek) { + const { weekNumber, dates } = this.getPrevMonthDays(firstDayOfWeek); + + const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM'); + const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`); + + const $weekNumber = this.createWeekNumber(weekNumber); + this.$month.append($weekNumber); + + dates.forEach(date => { + const tempDate = this.date; + this.date = date; + const $day = this.createDay(dateNotesForPrevMonth, date.date()); + $day.addClass('calendar-date-prev-month'); + this.$month.append($day); + this.date = tempDate; + }); + } + + const currentMonth = this.date.month(); + + while (this.date.month() === currentMonth) { + const weekNumber = this.getWeekNumber(this.date); + + // Add week number if it's first day of week + if (this.date.day() === this.firstDayOfWeek) { + const $weekNumber = this.createWeekNumber(weekNumber); + this.$month.append($weekNumber); + } + + const $day = this.createDay(dateNotesForMonth, this.date.date()); this.$month.append($day); - this.date.setDate(this.date.getDate() + 1); + this.date = this.date.add(1, 'day'); } // while loop trips over and day is at 30/31, bring it back - this.date.setDate(1); - this.date.setMonth(this.date.getMonth() - 1); + this.date = this.date.startOf('month').subtract(1, 'month'); - this.$monthSelect.text(MONTHS[this.date.getMonth()]); - this.$yearSelect.val(this.date.getFullYear()); + // Add dates from next month + const lastDayOfMonth = this.date.endOf('month'); + const lastDayOfWeek = lastDayOfMonth.day(); + const lastDayOfUserWeek = (this.firstDayOfWeek + 6) % 7; + if (lastDayOfWeek !== lastDayOfUserWeek) { + const dates = this.getNextMonthDays(lastDayOfWeek); + + const nextMonth = this.date.add(1, 'month').format('YYYY-MM'); + const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`); + + dates.forEach(date => { + const tempDate = this.date; + this.date = date; + const $day = this.createDay(dateNotesForNextMonth, date.date()); + $day.addClass('calendar-date-next-month'); + this.$month.append($day); + this.date = tempDate; + }); + } + + this.$monthSelect.text(MONTHS[this.date.month()]); + this.$yearSelect.val(this.date.year()); } async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (!loadResults.getOptionNames().includes("firstDayOfWeek")) { + if (!loadResults.getOptionNames().includes("firstDayOfWeek") && + !loadResults.getOptionNames().includes("firstWeekOfYear") && + !loadResults.getOptionNames().includes("minDaysInFirstWeek")) { return; } this.manageFirstDayOfWeek(); + this.initWeekCalculation(); this.createMonth(); } } diff --git a/src/public/app/widgets/type_widgets/options/i18n/i18n.ts b/src/public/app/widgets/type_widgets/options/i18n/i18n.ts index 27389eb64..b75c99fc2 100644 --- a/src/public/app/widgets/type_widgets/options/i18n/i18n.ts +++ b/src/public/app/widgets/type_widgets/options/i18n/i18n.ts @@ -35,6 +35,41 @@ const TPL = /*html*/` +
        + +
        + + + + + +
        +
        + + + +

        ${t("i18n.first-week-info")}

        + + +
        @@ -64,6 +99,16 @@ const TPL = /*html*/` .locale-options-container .option-row.centered { justify-content: center; } + + .locale-options-container .option-row [aria-labelledby="first-week-of-year-label"] { + display: flex; + flex-direction: column; + } + + .locale-options-container .option-row [aria-labelledby="first-week-of-year-label"] .tn-radio { + margin-left: 0; + white-space: nowrap; + } `; @@ -72,10 +117,13 @@ export default class LocalizationOptions extends OptionsWidget { private $localeSelect!: JQuery; private $formattingLocaleSelect!: JQuery; + private $minDaysRow!: JQuery; doRender() { this.$widget = $(TPL); + this.$minDaysRow = this.$widget.find(".min-days-row"); + this.$localeSelect = this.$widget.find(".locale-select"); this.$localeSelect.on("change", async () => { const newLocale = this.$localeSelect.val(); @@ -92,6 +140,30 @@ export default class LocalizationOptions extends OptionsWidget { const firstDayOfWeek = String(this.$widget.find(`input[name="first-day-of-week"]:checked`).val()); this.updateOption("firstDayOfWeek", firstDayOfWeek); }); + + this.$widget.find('input[name="first-week-of-year"]').on('change', (e) => { + const target = e.target as HTMLInputElement; + const value = parseInt(target.value); + + if (value === 2) { + this.$minDaysRow.show(); + } else { + this.$minDaysRow.hide(); + } + + this.updateOption("firstWeekOfYear", value); + }); + + const currentValue = this.$widget.find('input[name="first-week-of-year"]:checked').val(); + if (currentValue === 2) { + this.$minDaysRow.show(); + } + + this.$widget.find("#min-days-in-first-week").on("change", () => { + const minDays = this.$widget.find("#min-days-in-first-week").val(); + this.updateOption("minDaysInFirstWeek", minDays); + }); + this.$widget.find(".restart-app-button").on("click", utils.restartDesktopApp); } @@ -119,6 +191,15 @@ export default class LocalizationOptions extends OptionsWidget { this.$formattingLocaleSelect.val(options.formattingLocale); this.$widget.find(`input[name="first-day-of-week"][value="${options.firstDayOfWeek}"]`) - .prop("checked", "true"); + .prop("checked", "true"); + + this.$widget.find(`input[name="first-week-of-year"][value="${options.firstWeekOfYear}"]`) + .prop("checked", "true"); + + if (parseInt(options.firstWeekOfYear) === 2) { + this.$minDaysRow.show(); + } + + this.$widget.find("#min-days-in-first-week").val(options.minDaysInFirstWeek); } } diff --git a/src/public/app/widgets/type_widgets/options/options_widget.ts b/src/public/app/widgets/type_widgets/options/options_widget.ts index fb4d9c0c3..5d614a42e 100644 --- a/src/public/app/widgets/type_widgets/options/options_widget.ts +++ b/src/public/app/widgets/type_widgets/options/options_widget.ts @@ -1,4 +1,4 @@ -import type { FilterOptionsByType, OptionDefinitions, OptionMap, OptionNames } from "../../../../../services/options_interface.js"; +import type { FilterOptionsByType, OptionMap, OptionNames } from "../../../../../services/options_interface.js"; import type { EventData, EventListener } from "../../../components/app_context.js"; import type FNote from "../../../entities/fnote.js"; import { t } from "../../../services/i18n.js"; @@ -45,7 +45,7 @@ export default class OptionsWidget extends NoteContextAwareWidget implements Eve $checkbox.prop("checked", optionValue === "true"); } - optionsLoaded(options: OptionMap) {} + optionsLoaded(options: OptionMap) { } async refresh() { this.toggleInt(this.isEnabled()); diff --git a/src/public/stylesheets/calendar.css b/src/public/stylesheets/calendar.css index e1c39e52a..a3ad89c34 100644 --- a/src/public/stylesheets/calendar.css +++ b/src/public/stylesheets/calendar.css @@ -35,7 +35,7 @@ padding: 0 0.5rem 0.5rem 0.5rem; } -.calendar-dropdown-widget .calendar-header > div { +.calendar-dropdown-widget .calendar-header>div { display: flex; justify-content: center; flex-grow: 1; @@ -67,7 +67,8 @@ } .calendar-dropdown-widget .calendar-header .dropdown-toggle::after { - border: unset; /* Disable the dropdown arrow */ + border: unset; + /* Disable the dropdown arrow */ } .calendar-dropdown-widget .calendar-week { @@ -77,10 +78,10 @@ .calendar-dropdown-widget .calendar-week span { flex-direction: column; - flex: 0 0 14.28%; + flex: 0 0 12.5%; font-size: 1rem; font-weight: bold; - max-width: 14.28%; + max-width: 12.5%; padding-top: 5px; padding-bottom: 5px; text-align: center; @@ -92,13 +93,40 @@ flex-wrap: wrap; } +.calendar-dropdown-widget .calendar-week-number { + color: var(--muted-text-color) !important; + position: relative; +} + +.calendar-dropdown-widget .calendar-week-number::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 1px; + background-color: var(--main-border-color); + z-index: 2; +} + +.calendar-dropdown-widget .calendar-week-number-disabled { + align-items: center; + color: var(--main-text-color); + display: flex; + flex-direction: column; + flex: 0 0 12.5%; + max-width: 12.5%; + padding: 0.4rem 0; + font-size: 120%; +} + .calendar-dropdown-widget .calendar-date { align-items: center; color: var(--main-text-color); display: flex; flex-direction: column; - flex: 0 0 14.28%; - max-width: 14.28%; + flex: 0 0 12.5%; + max-width: 12.5%; padding: 0.4rem 0; font-size: 120%; } @@ -129,3 +157,17 @@ .calendar-dropdown-widget .calendar-date:not(.calendar-date-active) { cursor: pointer; } + +.calendar-dropdown-widget .calendar-date-prev-month, +.calendar-dropdown-widget .calendar-date-next-month { + color: var(--muted-text-color); + opacity: 0.6; +} + +.calendar-dropdown-widget .calendar-date-prev-month:hover, +.calendar-dropdown-widget .calendar-date-next-month:hover { + opacity: 1; + background-color: var(--hover-item-background-color); + color: var(--hover-item-text-color); + text-decoration: underline; +} \ No newline at end of file diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index dc0746b4c..da8f3bb1d 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -334,7 +334,6 @@ body.layout-horizontal > .horizontal { } .calendar-dropdown-widget .calendar-header { - padding: 8px 0 20px 0; gap: 10px; } diff --git a/src/public/translations/cn/translation.json b/src/public/translations/cn/translation.json index 5f9403591..74751c510 100644 --- a/src/public/translations/cn/translation.json +++ b/src/public/translations/cn/translation.json @@ -588,6 +588,7 @@ "sat": "六", "sun": "日", "cannot_find_day_note": "无法找到日记", + "cannot_find_week_note": "无法找到周记", "january": "一月", "febuary": "二月", "march": "三月", @@ -1231,7 +1232,15 @@ "language": "语言", "first-day-of-the-week": "一周的第一天", "sunday": "周日", - "monday": "周一" + "monday": "周一", + "first-week-of-the-year": "一年的第一周", + "first-week-contains-first-day": "第一周包含一年的第一天", + "first-week-contains-first-thursday": "第一周包含一年的第一个周四", + "first-week-has-minimum-days": "第一周有最小天数", + "min-days-in-first-week": "第一周的最小天数", + "first-week-info": "第一周包含一年的第一个周四,基于
        ISO 8601 标准。", + "first-week-warning": "更改第一周选项可能会导致与现有周笔记重复,已创建的周笔记将不会相应更新。", + "formatting-locale": "日期和数字格式" }, "backup": { "automatic_backup": "自动备份", diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 1b67cbc7a..45c934f5e 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -588,6 +588,7 @@ "sat": "Sat", "sun": "Sun", "cannot_find_day_note": "Cannot find day note", + "cannot_find_week_note": "Cannot find week note", "january": "January", "febuary": "February", "march": "March", @@ -1242,6 +1243,13 @@ "first-day-of-the-week": "First day of the week", "sunday": "Sunday", "monday": "Monday", + "first-week-of-the-year": "First week of the year", + "first-week-contains-first-day": "First week contains first day of the year", + "first-week-contains-first-thursday": "First week contains first Thursday of the year", + "first-week-has-minimum-days": "First week has minimum days", + "min-days-in-first-week": "Minimum days in first week", + "first-week-info": "First week contains first Thursday of the year is based on ISO 8601 standard.", + "first-week-warning": "Changing first week options may cause duplicate with existing Week Notes and the existing Week Notes will not be updated accordingly.", "formatting-locale": "Date & number format" }, "backup": { diff --git a/src/routes/api/clipper.ts b/src/routes/api/clipper.ts index 9f1510fcb..2535a26e2 100644 --- a/src/routes/api/clipper.ts +++ b/src/routes/api/clipper.ts @@ -1,23 +1,21 @@ -"use strict"; - import type { Request } from "express"; - -import attributeService from "../../services/attributes.js"; -import cloneService from "../../services/cloning.js"; -import noteService from "../../services/notes.js"; -import dateNoteService from "../../services/date_notes.js"; -import dateUtils from "../../services/date_utils.js"; -import imageService from "../../services/image.js"; -import appInfo from "../../services/app_info.js"; -import ws from "../../services/ws.js"; -import log from "../../services/log.js"; -import utils from "../../services/utils.js"; -import path from "path"; -import htmlSanitizer from "../../services/html_sanitizer.js"; -import attributeFormatter from "../../services/attribute_formatter.js"; import jsdom from "jsdom"; +import path from "path"; + import type BNote from "../../becca/entities/bnote.js"; import ValidationError from "../../errors/validation_error.js"; +import appInfo from "../../services/app_info.js"; +import attributeFormatter from "../../services/attribute_formatter.js"; +import attributeService from "../../services/attributes.js"; +import cloneService from "../../services/cloning.js"; +import dateNoteService from "../../services/date_notes.js"; +import dateUtils from "../../services/date_utils.js"; +import htmlSanitizer from "../../services/html_sanitizer.js"; +import imageService from "../../services/image.js"; +import log from "../../services/log.js"; +import noteService from "../../services/notes.js"; +import utils from "../../services/utils.js"; +import ws from "../../services/ws.js"; const { JSDOM } = jsdom; interface Image { @@ -26,14 +24,14 @@ interface Image { imageId: string; } -function addClipping(req: Request) { +async function addClipping(req: Request) { // if a note under the clipperInbox has the same 'pageUrl' attribute, // add the content to that note and clone it under today's inbox // otherwise just create a new note under today's inbox const { title, content, images } = req.body; const clipType = "clippings"; - const clipperInbox = getClipperInboxNote(); + const clipperInbox = await getClipperInboxNote(); const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl); let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType); @@ -89,17 +87,17 @@ function findClippingNote(clipperInboxNote: BNote, pageUrl: string, clipType: st return clipType ? notes.find((note) => note.getOwnedLabelValue("clipType") === clipType) : notes[0]; } -function getClipperInboxNote() { +async function getClipperInboxNote() { let clipperInbox = attributeService.getNoteWithLabel("clipperInbox"); if (!clipperInbox) { - clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate()); + clipperInbox = await dateNoteService.getDayNote(dateUtils.localNowDate()); } return clipperInbox; } -function createNote(req: Request) { +async function createNote(req: Request) { const { content, images, labels } = req.body; const clipType = htmlSanitizer.sanitize(req.body.clipType); @@ -108,7 +106,7 @@ function createNote(req: Request) { const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : ""; const title = trimmedTitle || `Clipped note from ${pageUrl}`; - const clipperInbox = getClipperInboxNote(); + const clipperInbox = await getClipperInboxNote(); let note = findClippingNote(clipperInbox, pageUrl, clipType); if (!note) { @@ -215,9 +213,9 @@ function handshake() { }; } -function findNotesByUrl(req: Request) { +async function findNotesByUrl(req: Request) { const pageUrl = req.params.noteUrl; - const clipperInbox = getClipperInboxNote(); + const clipperInbox = await getClipperInboxNote(); const foundPage = findClippingNote(clipperInbox, pageUrl, null); return { noteId: foundPage ? foundPage.noteId : null diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 140e2448e..aeb4e9009 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -72,6 +72,8 @@ const ALLOWED_OPTIONS = new Set([ "locale", "formattingLocale", "firstDayOfWeek", + "firstWeekOfYear", + "minDaysInFirstWeek", "languages", "textNoteEditorType", "textNoteEditorMultilineToolbar", diff --git a/src/routes/api/sender.ts b/src/routes/api/sender.ts index d9b1dbf29..efdf6817a 100644 --- a/src/routes/api/sender.ts +++ b/src/routes/api/sender.ts @@ -1,11 +1,10 @@ -"use strict"; - +import type { Request } from "express"; import imageType from "image-type"; + import imageService from "../../services/image.js"; import noteService from "../../services/notes.js"; import sanitizeAttributeName from "../../services/sanitize_attribute_name.js"; import specialNotesService from "../../services/special_notes.js"; -import type { Request } from "express"; async function uploadImage(req: Request) { const file = req.file; @@ -34,7 +33,7 @@ async function uploadImage(req: Request) { return [400, "Invalid local date"]; } - const parentNote = specialNotesService.getInboxNote(req.headers["x-local-date"]); + const parentNote = await specialNotesService.getInboxNote(req.headers["x-local-date"]); const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true); @@ -55,12 +54,12 @@ async function uploadImage(req: Request) { }; } -function saveNote(req: Request) { +async function saveNote(req: Request) { if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) { return [400, "Invalid local date"]; } - const parentNote = specialNotesService.getInboxNote(req.headers["x-local-date"]); + const parentNote = await specialNotesService.getInboxNote(req.headers["x-local-date"]); const { note, branch } = noteService.createNewNote({ parentNoteId: parentNote.noteId, diff --git a/src/routes/api/special_notes.ts b/src/routes/api/special_notes.ts index b884c3291..2c87e8098 100644 --- a/src/routes/api/special_notes.ts +++ b/src/routes/api/special_notes.ts @@ -1,5 +1,3 @@ -"use strict"; - import dateNoteService from "../../services/date_notes.js"; import sql from "../../services/sql.js"; import cls from "../../services/cls.js"; @@ -15,14 +13,22 @@ function getDayNote(req: Request) { return dateNoteService.getDayNote(req.params.date); } +function getWeekFirstDayNote(req: Request) { + return dateNoteService.getWeekFirstDayNote(req.params.date); +} + function getWeekNote(req: Request) { - return dateNoteService.getWeekNote(req.params.date); + return dateNoteService.getWeekNote(req.params.week); } function getMonthNote(req: Request) { return dateNoteService.getMonthNote(req.params.month); } +function getQuarterNote(req: Request) { + return dateNoteService.getQuarterNote(req.params.quarter); +} + function getYearNote(req: Request) { return dateNoteService.getYearNote(req.params.year); } @@ -58,8 +64,8 @@ function getDayNotesForMonth(req: Request) { } } -function saveSqlConsole(req: Request) { - return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId); +async function saveSqlConsole(req: Request) { + return await specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId); } function createSqlConsole() { @@ -101,8 +107,10 @@ function createOrUpdateScriptLauncherFromApi(req: Request) { export default { getInboxNote, getDayNote, + getWeekFirstDayNote, getWeekNote, getMonthNote, + getQuarterNote, getYearNote, getDayNotesForMonth, createSqlConsole, diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 37bca7988..cd49f5a6e 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,5 +1,3 @@ -"use strict"; - import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js"; import multer from "multer"; import log from "../services/log.js"; @@ -308,8 +306,10 @@ function register(app: express.Application) { apiRoute(GET, "/api/special-notes/inbox/:date", specialNotesRoute.getInboxNote); apiRoute(GET, "/api/special-notes/days/:date", specialNotesRoute.getDayNote); - apiRoute(GET, "/api/special-notes/weeks/:date", specialNotesRoute.getWeekNote); + apiRoute(GET, "/api/special-notes/week-first-day/:date", specialNotesRoute.getWeekFirstDayNote); + apiRoute(GET, "/api/special-notes/weeks/:week", specialNotesRoute.getWeekNote); apiRoute(GET, "/api/special-notes/months/:month", specialNotesRoute.getMonthNote); + apiRoute(GET, "/api/special-notes/quarters/:quarter", specialNotesRoute.getQuarterNote); apiRoute(GET, "/api/special-notes/years/:year", specialNotesRoute.getYearNote); apiRoute(GET, "/api/special-notes/notes-for-month/:month", specialNotesRoute.getDayNotesForMonth); apiRoute(PST, "/api/special-notes/sql-console", specialNotesRoute.createSqlConsole); diff --git a/src/services/backend_script_api.ts b/src/services/backend_script_api.ts index 42b56033f..f1f14bb0f 100644 --- a/src/services/backend_script_api.ts +++ b/src/services/backend_script_api.ts @@ -224,14 +224,14 @@ interface Api { * @param date in YYYY-MM-DD format * @param rootNote - specify calendar root note, normally leave empty to use the default calendar */ - getDayNote(date: string, rootNote?: BNote): BNote | null; + getDayNote(date: string, rootNote?: BNote): Promise; /** * Returns today's day note. If such note doesn't exist, it is created. * * @param rootNote specify calendar root note, normally leave empty to use the default calendar */ - getTodayNote(rootNote?: BNote): BNote | null; + getTodayNote(rootNote?: BNote): Promise; /** * Returns note for the first date of the week of the given date. @@ -239,15 +239,15 @@ interface Api { * @param date in YYYY-MM-DD format * @param rootNote - specify calendar root note, normally leave empty to use the default calendar */ - getWeekNote( - date: string, - options: { - // TODO: Deduplicate type with date_notes.ts once ES modules are added. - /** either "monday" (default) or "sunday" */ - startOfTheWeek: "monday" | "sunday"; - }, - rootNote: BNote - ): BNote | null; + getWeekFirstDayNote(date: string, rootNote: BNote): Promise; + + /** + * Returns week note for given date. If such a note doesn't exist, it is created. + * + * @param date in YYYY-MM-DD format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getWeekNote(date: string, rootNote: BNote): Promise; /** * Returns month note for given date. If such a note doesn't exist, it is created. @@ -255,7 +255,15 @@ interface Api { * @param date in YYYY-MM format * @param rootNote - specify calendar root note, normally leave empty to use the default calendar */ - getMonthNote(date: string, rootNote: BNote): BNote | null; + getMonthNote(date: string, rootNote: BNote): Promise; + + /** + * Returns quarter note for given date. If such a note doesn't exist, it is created. + * + * @param date in YYYY-MM format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getQuarterNote(date: string, rootNote: BNote): Promise; /** * Returns year note for given year. If such a note doesn't exist, it is created. @@ -552,8 +560,10 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) { this.getRootCalendarNote = dateNoteService.getRootCalendarNote; this.getDayNote = dateNoteService.getDayNote; this.getTodayNote = dateNoteService.getTodayNote; + this.getWeekFirstDayNote = dateNoteService.getWeekFirstDayNote; this.getWeekNote = dateNoteService.getWeekNote; this.getMonthNote = dateNoteService.getMonthNote; + this.getQuarterNote = dateNoteService.getQuarterNote; this.getYearNote = dateNoteService.getYearNote; this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(parentNoteId, sortConfig.sortBy || "title", !!sortConfig.reverse, !!sortConfig.foldersFirst); @@ -685,5 +695,5 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) { } export default BackendScriptApi as any as { - new (currentNote: BNote, apiParams: ApiParams): Api; + new(currentNote: BNote, apiParams: ApiParams): Api; }; diff --git a/src/services/builtin_attributes.ts b/src/services/builtin_attributes.ts index 1caaecd63..86fa35a9d 100644 --- a/src/services/builtin_attributes.ts +++ b/src/services/builtin_attributes.ts @@ -36,6 +36,12 @@ export default [ { type: "label", name: "workspaceSearchHome" }, { type: "label", name: "sqlConsoleHome" }, { type: "label", name: "datePattern" }, + { type: "label", name: "weekPattern" }, + { type: "label", name: "enableWeekNote" }, + { type: "label", name: "monthPattern" }, + { type: "label", name: "quarterPattern" }, + { type: "label", name: "yearPattern" }, + { type: "label", name: "enableQuarterNote" }, { type: "label", name: "pageSize" }, { type: "label", name: "viewType" }, { type: "label", name: "mapRootNoteId" }, diff --git a/src/services/date_notes.spec.ts b/src/services/date_notes.spec.ts new file mode 100644 index 000000000..0366493ce --- /dev/null +++ b/src/services/date_notes.spec.ts @@ -0,0 +1,114 @@ +import dayjs from "dayjs"; +import i18next from "i18next"; +import { beforeAll,describe, expect, it, vi } from 'vitest'; + +import type BNote from "../becca/entities/bnote.js"; +import dateNotesService from "./date_notes.js"; + +// Mock becca_loader +vi.mock("../becca/becca_loader.js", () => ({ + default: { + load: vi.fn(), + loaded: Promise.resolve() + } +})); + +// Mock SQL init +vi.mock("../services/sql.js", () => ({ + default: { + dbReady: Promise.resolve(), + transactional: vi.fn((callback) => callback()) + } +})); + + +// Mock BNote +const mockRootNote = { + getOwnedLabelValue: (key: string) => { + const patterns: Record = { + "yearPattern": "{year}", + "quarterPattern": "Quarter {quarterNumber}", + "monthPattern": "{monthNumberPadded} - {month}", + "weekPattern": "Week {weekNumber}", + "datePattern": "{dateNumberPadded} - {weekDay}" + }; + return patterns[key] || null; + } +} as unknown as BNote; + +describe("date_notes", () => { + beforeAll(async () => { + await i18next.init({ + lng: "en", + resources: { + en: { + translation: { + "months.march": "March", + "weekdays.saturday": "Saturday" + } + } + } + }); + }); + + describe("getJournalNoteTitle", () => { + const testDate = dayjs("2025-03-15"); // Saturday + + it("should generate year note title", async () => { + const title = await dateNotesService.getJournalNoteTitle(mockRootNote, "year", testDate, 2025); + expect(title).toBe("2025"); + }); + + it("should generate quarter note title", async () => { + const title = await dateNotesService.getJournalNoteTitle(mockRootNote, "quarter", testDate, 1); + expect(title).toBe("Quarter 1"); + }); + + it("should generate month note title", async () => { + const title = await dateNotesService.getJournalNoteTitle(mockRootNote, "month", testDate, 3); + expect(title).toBe("03 - March"); + }); + + it("should generate week note title", async () => { + const title = await dateNotesService.getJournalNoteTitle(mockRootNote, "week", testDate, 11); + expect(title).toBe("Week 11"); + }); + + it("should generate day note title", async () => { + const title = await dateNotesService.getJournalNoteTitle(mockRootNote, "day", testDate, 15); + expect(title).toBe("15 - Saturday"); + }); + + it("should respect custom patterns", async () => { + const customRootNote = { + getOwnedLabelValue: (key: string) => { + const patterns: Record = { + "yearPattern": "{year}", + "quarterPattern": "{quarterNumber} {shortQuarter}", + "monthPattern": "{isoMonth} {monthNumber} {monthNumberPadded} {month} {shortMonth3} {shortMonth4}", + "weekPattern": "{weekNumber} {weekNumberPadded} {shortWeek} {shortWeek3}", + "datePattern": "{isoDate} {dateNumber} {dateNumberPadded} {ordinal} {weekDay} {weekDay3} {weekDay2}" + }; + return patterns[key] || null; + } + } as unknown as BNote; + + const testDate = dayjs("2025-03-01"); // Saturday + + const yearTitle = await dateNotesService.getJournalNoteTitle(customRootNote, "year", testDate, 2025); + expect(yearTitle).toBe("2025"); + + const quarterTitle = await dateNotesService.getJournalNoteTitle(customRootNote, "quarter", testDate, 1); + expect(quarterTitle).toBe("1 Q1"); + + const monthTitle = await dateNotesService.getJournalNoteTitle(customRootNote, "month", testDate, 3); + expect(monthTitle).toBe("2025-03 3 03 March Mar Marc"); + + const weekTitle = await dateNotesService.getJournalNoteTitle(customRootNote, "week", testDate, 9); + expect(weekTitle).toBe("9 09 W9 W09"); + + const dayTitle = await dateNotesService.getJournalNoteTitle(customRootNote, "day", testDate, 1); + expect(dayTitle).toBe("2025-03-01 1 01 1st Saturday Sat Sa"); + }); + }); +}); diff --git a/src/services/date_notes.ts b/src/services/date_notes.ts index 1324de89b..44c5b07aa 100644 --- a/src/services/date_notes.ts +++ b/src/services/date_notes.ts @@ -1,22 +1,38 @@ -"use strict"; - -import noteService from "./notes.js"; -import attributeService from "./attributes.js"; -import dateUtils from "./date_utils.js"; -import sql from "./sql.js"; -import protectedSessionService from "./protected_session.js"; -import searchService from "../services/search/services/search.js"; -import SearchContext from "../services/search/search_context.js"; -import hoistedNoteService from "./hoisted_note.js"; import type BNote from "../becca/entities/bnote.js"; +import type { Dayjs } from "dayjs"; + +import advancedFormat from "dayjs/plugin/advancedFormat.js"; +import attributeService from "./attributes.js"; +import cloningService from "./cloning.js"; +import dayjs from "dayjs"; +import hoistedNoteService from "./hoisted_note.js"; +import i18next from "i18next"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js"; +import noteService from "./notes.js"; +import optionService from "./options.js"; +import protectedSessionService from "./protected_session.js"; +import quarterOfYear from "dayjs/plugin/quarterOfYear.js"; +import searchContext from "../services/search/search_context.js"; +import searchService from "../services/search/services/search.js"; +import sql from "./sql.js"; import { t } from "i18next"; +dayjs.extend(isSameOrAfter); +dayjs.extend(quarterOfYear); +dayjs.extend(advancedFormat); + const CALENDAR_ROOT_LABEL = "calendarRoot"; const YEAR_LABEL = "yearNote"; +const QUARTER_LABEL = "quarterNote"; const MONTH_LABEL = "monthNote"; +const WEEK_LABEL = "weekNote"; const DATE_LABEL = "dateNote"; -const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"]; +const WEEKDAY_TRANSLATION_IDS = [ + "weekdays.sunday", "weekdays.monday", "weekdays.tuesday", + "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", + "weekdays.saturday", "weekdays.sunday" +]; const MONTH_TRANSLATION_IDS = [ "months.january", @@ -33,14 +49,114 @@ const MONTH_TRANSLATION_IDS = [ "months.december" ]; -type StartOfWeek = "monday" | "sunday"; +type TimeUnit = "year" | "quarter" | "month" | "week" | "day"; + +const baseReplacements = { + year: [ "year" ], + quarter: [ "quarterNumber", "shortQuarter" ], + month: [ "isoMonth", "monthNumber", "monthNumberPadded", + "month", "shortMonth3", "shortMonth4" ], + week: [ "weekNumber", "weekNumberPadded", "shortWeek", "shortWeek3" ], + day: [ "isoDate", "dateNumber", "dateNumberPadded", + "ordinal", "weekDay", "weekDay3", "weekDay2" ] +}; + +function getTimeUnitReplacements(timeUnit: TimeUnit): string[] { + const units: TimeUnit[] = [ "year", "quarter", "month", "week", "day" ]; + const index = units.indexOf(timeUnit); + return units.slice(0, index + 1).flatMap(unit => baseReplacements[unit]); +} + +async function ordinal(date: Dayjs, lng: string) { + const localeMap: Record = { + cn: "zh-cn", + tw: "zh-tw" + }; + + const dayjsLocale = localeMap[lng] || lng; + + try { + await import(`dayjs/locale/${dayjsLocale}.js`); + } catch (err) { + console.warn(`Could not load locale ${dayjsLocale}`, err); + } + + return dayjs(date).locale(dayjsLocale).format("Do"); +} + +async function getJournalNoteTitle( + rootNote: BNote, + timeUnit: TimeUnit, + dateObj: Dayjs, + number: number +) { + const patterns = { + year: rootNote.getOwnedLabelValue("yearPattern") || "{year}", + quarter: rootNote.getOwnedLabelValue("quarterPattern") || t("quarterNumber"), + month: rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}", + week: rootNote.getOwnedLabelValue("weekPattern") || t("weekdayNumber"), + day: rootNote.getOwnedLabelValue("datePattern") || "{dateNumberPadded} - {weekDay}" + }; + + const pattern = patterns[timeUnit]; + const monthName = t(MONTH_TRANSLATION_IDS[dateObj.month()]); + const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.day()]); + const numberStr = number.toString(); + const ordinalStr = await ordinal(dateObj, i18next.language); + + const allReplacements: Record = { + // Common date formats + "{year}": dateObj.format("YYYY"), + + // Month related + "{isoMonth}": dateObj.format("YYYY-MM"), + "{monthNumber}": numberStr, + "{monthNumberPadded}": numberStr.padStart(2, "0"), + "{month}": monthName, + "{shortMonth3}": monthName.slice(0, 3), + "{shortMonth4}": monthName.slice(0, 4), + + // Quarter related + "{quarterNumber}": numberStr, + "{shortQuarter}": `Q${numberStr}`, + + // Week related + "{weekNumber}": numberStr, + "{weekNumberPadded}": numberStr.padStart(2, "0"), + "{shortWeek}": `W${numberStr}`, + "{shortWeek3}": `W${numberStr.padStart(2, "0")}`, + + // Day related + "{isoDate}": dateObj.format("YYYY-MM-DD"), + "{dateNumber}": numberStr, + "{dateNumberPadded}": numberStr.padStart(2, "0"), + "{ordinal}": ordinalStr, + "{weekDay}": weekDay, + "{weekDay3}": weekDay.substring(0, 3), + "{weekDay2}": weekDay.substring(0, 2) + }; + + const allowedReplacements = Object.entries(allReplacements).reduce((acc, [ key, value ]) => { + const replacementKey = key.slice(1, -1); + if (getTimeUnitReplacements(timeUnit).includes(replacementKey)) { + acc[key] = value; + } + return acc; + }, {} as Record); + + return Object.entries(allowedReplacements).reduce( + (title, [ key, value ]) => title.replace(new RegExp(key, "g"), value), + pattern + ); +} function createNote(parentNote: BNote, noteTitle: string) { return noteService.createNewNote({ parentNoteId: parentNote.noteId, title: noteTitle, content: "", - isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), + isProtected: parentNote.isProtected && + protectedSessionService.isProtectedSessionAvailable(), type: "text" }).note; } @@ -51,7 +167,9 @@ function getRootCalendarNote(): BNote { const workspaceNote = hoistedNoteService.getWorkspaceNote(); if (!workspaceNote || !workspaceNote.isRoot()) { - rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false })); + rootNote = searchService.findFirstNoteWithQuery( + "#workspaceCalendarRoot", new searchContext({ ignoreHoistedNote: false }) + ); } if (!rootNote) { @@ -80,9 +198,11 @@ function getRootCalendarNote(): BNote { function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote { const rootNote = _rootNote || getRootCalendarNote(); - const yearStr = dateStr.trim().substr(0, 4); + const yearStr = dateStr.trim().substring(0, 4); - let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId })); + let yearNote = searchService.findFirstNoteWithQuery( + `#${YEAR_LABEL}="${yearStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId }) + ); if (yearNote) { return yearNote; @@ -104,38 +224,79 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote { return yearNote as unknown as BNote; } -function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date) { - const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}"; - const monthName = t(MONTH_TRANSLATION_IDS[dateObj.getMonth()]); - - return pattern - .replace(/{shortMonth3}/g, monthName.slice(0, 3)) - .replace(/{shortMonth4}/g, monthName.slice(0, 4)) - .replace(/{isoMonth}/g, dateUtils.utcDateStr(dateObj).slice(0, 7)) - .replace(/{monthNumberPadded}/g, monthNumber) - .replace(/{month}/g, monthName); +function getQuarterNumberStr(date: Dayjs) { + return `${date.year()}-Q${date.quarter()}`; } -function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote { +async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): Promise { const rootNote = _rootNote || getRootCalendarNote(); - const monthStr = dateStr.substr(0, 7); - const monthNumber = dateStr.substr(5, 2); + quarterStr = quarterStr.trim().substring(0, 7); - let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId })); + let quarterNote = searchService.findFirstNoteWithQuery( + `#${QUARTER_LABEL}="${quarterStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId }) + ); + + if (quarterNote) { + return quarterNote; + } + + const [ yearStr, quarterNumberStr ] = quarterStr.trim().split("-Q"); + const quarterNumber = parseInt(quarterNumberStr); + const firstMonth = (quarterNumber - 1) * 3; + const quarterStartDate = dayjs().year(parseInt(yearStr)).month(firstMonth).date(1); + + const yearNote = getYearNote(yearStr, rootNote); + const noteTitle = await getJournalNoteTitle( + rootNote, "quarter", quarterStartDate, quarterNumber + ); + + sql.transactional(() => { + quarterNote = createNote(yearNote, noteTitle); + + attributeService.createLabel(quarterNote.noteId, QUARTER_LABEL, quarterStr); + attributeService.createLabel(quarterNote.noteId, "sorted"); + + const quarterTemplateAttr = rootNote.getOwnedAttribute("relation", "quarterTemplate"); + + if (quarterTemplateAttr) { + attributeService.createRelation( + quarterNote.noteId, "template", quarterTemplateAttr.value + ); + } + }); + + return quarterNote as unknown as BNote; +} + +async function getMonthNote(dateStr: string, _rootNote: BNote | null = null): Promise { + const rootNote = _rootNote || getRootCalendarNote(); + + const monthStr = dateStr.substring(0, 7); + const monthNumber = dateStr.substring(5, 7); + + let monthNote = searchService.findFirstNoteWithQuery( + `#${MONTH_LABEL}="${monthStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId }) + ); if (monthNote) { return monthNote; } - const dateObj = dateUtils.parseLocalDate(dateStr); + let monthParentNote; - const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj); + if (rootNote.hasLabel("enableQuarterNote")) { + monthParentNote = await getQuarterNote(getQuarterNumberStr(dayjs(dateStr)), rootNote); + } else { + monthParentNote = getYearNote(dateStr, rootNote); + } - const yearNote = getYearNote(dateStr, rootNote); + const noteTitle = await getJournalNoteTitle( + rootNote, "month", dayjs(dateStr), parseInt(monthNumber) + ); sql.transactional(() => { - monthNote = createNote(yearNote, noteTitle); + monthNote = createNote(monthParentNote, noteTitle); attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr); attributeService.createLabel(monthNote.noteId, "sorted"); @@ -150,49 +311,178 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote { return monthNote as unknown as BNote; } -function getDayNoteTitle(rootNote: BNote, dayNumber: string, dateObj: Date) { - const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"; - const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.getDay()]); +function getWeekStartDate(date: Dayjs): Dayjs { + const day = date.day(); + let diff; - return pattern - .replace(/{ordinal}/g, ordinal(parseInt(dayNumber))) - .replace(/{dayInMonthPadded}/g, dayNumber) - .replace(/{isoDate}/g, dateUtils.utcDateStr(dateObj)) - .replace(/{weekDay}/g, weekDay) - .replace(/{weekDay3}/g, weekDay.substr(0, 3)) - .replace(/{weekDay2}/g, weekDay.substr(0, 2)); + if (optionService.getOption("firstDayOfWeek") === "0") { // Sunday + diff = date.date() - day + (day === 0 ? -6 : 1); // adjust when day is sunday + } else { // Monday + diff = date.date() - day; + } + + const startDate = date.clone().date(diff); + return startDate; } -/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */ -function ordinal(dayNumber: number) { - const suffixes = ["th", "st", "nd", "rd"]; - const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0]; +// TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts +// Maybe can be merged later in monorepo setup +function getWeekNumberStr(date: Dayjs): string { + const year = date.year(); + const dayOfWeek = (day: number) => + (day - parseInt(optionService.getOption("firstDayOfWeek")) + 7) % 7; - return `${dayNumber}${suffix}`; + // Get first day of the year and adjust to first week start + const jan1 = date.clone().year(year).month(0).date(1); + const jan1Weekday = jan1.day(); + const dayOffset = dayOfWeek(jan1Weekday); + let firstWeekStart = jan1.clone().subtract(dayOffset, "day"); + + // Adjust based on week rule + switch (parseInt(optionService.getOption("firstWeekOfYear"))) { + case 1: { // ISO 8601: first week contains Thursday + const thursday = firstWeekStart.clone().add(3, "day"); // Monday + 3 = Thursday + if (thursday.year() < year) { + firstWeekStart = firstWeekStart.add(7, "day"); + } + break; + } + case 2: { // minDaysInFirstWeek rule + const daysInFirstWeek = 7 - dayOffset; + if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) { + firstWeekStart = firstWeekStart.add(7, "day"); + } + break; + } + // default case 0: week containing Jan 1 → already handled + } + + const diffDays = date.startOf("day").diff(firstWeekStart.startOf("day"), "day"); + const weekNumber = Math.floor(diffDays / 7) + 1; + + // Handle case when date is before first week start → belongs to last week of previous year + if (weekNumber <= 0) { + return getWeekNumberStr(date.subtract(1, "day")); + } + + // Handle case when date belongs to first week of next year + const nextYear = year + 1; + const jan1Next = date.clone().year(nextYear).month(0).date(1); + const jan1WeekdayNext = jan1Next.day(); + const offsetNext = dayOfWeek(jan1WeekdayNext); + let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, "day"); + + switch (parseInt(optionService.getOption("firstWeekOfYear"))) { + case 1: { + const thursday = nextYearWeekStart.clone().add(3, "day"); + if (thursday.year() < nextYear) { + nextYearWeekStart = nextYearWeekStart.add(7, "day"); + } + break; + } + case 2: { + const daysInFirstWeek = 7 - offsetNext; + if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) { + nextYearWeekStart = nextYearWeekStart.add(7, "day"); + } + break; + } + } + + if (date.isSameOrAfter(nextYearWeekStart)) { + return `${nextYear}-W01`; + } + + return `${year}-W${weekNumber.toString().padStart(2, "0")}`; } -function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote { +function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) { + const weekStartDate = getWeekStartDate(dayjs(dateStr)); + return getDayNote(weekStartDate.format("YYYY-MM-DD"), rootNote); +} + +async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Promise { + const rootNote = _rootNote || getRootCalendarNote(); + if (!rootNote.hasLabel("enableWeekNote")) { + return null; + } + + weekStr = weekStr.trim().substring(0, 8); + + let weekNote = searchService.findFirstNoteWithQuery( + `#${WEEK_LABEL}="${weekStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId }) + ); + + if (weekNote) { + return weekNote; + } + + const [ yearStr, weekNumStr ] = weekStr.trim().split("-W"); + const weekNumber = parseInt(weekNumStr); + + 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 = await getMonthNote(startDate.format("YYYY-MM-DD"), rootNote); + const noteTitle = await getJournalNoteTitle(rootNote, "week", startDate, weekNumber); + + sql.transactional(async () => { + weekNote = createNote(monthNote, noteTitle); + + attributeService.createLabel(weekNote.noteId, WEEK_LABEL, weekStr); + attributeService.createLabel(weekNote.noteId, "sorted"); + + const weekTemplateAttr = rootNote.getOwnedAttribute("relation", "weekTemplate"); + + if (weekTemplateAttr) { + attributeService.createRelation(weekNote.noteId, "template", weekTemplateAttr.value); + } + + // If the week spans different months, clone the week note in the other month as well + if (startMonth !== endMonth) { + const secondMonthNote = await getMonthNote(endDate.format("YYYY-MM-DD"), rootNote); + cloningService.cloneNoteToParentNote(weekNote.noteId, secondMonthNote.noteId); + } + }); + + return weekNote as unknown as BNote; +} + +async function getDayNote(dateStr: string, _rootNote: BNote | null = null): Promise { const rootNote = _rootNote || getRootCalendarNote(); - dateStr = dateStr.trim().substr(0, 10); + dateStr = dateStr.trim().substring(0, 10); - let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId })); + let dateNote = searchService.findFirstNoteWithQuery( + `#${DATE_LABEL}="${dateStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId }) + ); if (dateNote) { return dateNote; } - const monthNote = getMonthNote(dateStr, rootNote); - const dayNumber = dateStr.substr(8, 2); + let dateParentNote; - const dateObj = dateUtils.parseLocalDate(dateStr); + if (rootNote.hasLabel("enableWeekNote")) { + dateParentNote = await getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote); + } else { + dateParentNote = await getMonthNote(dateStr, rootNote); + } - const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj); + const dayNumber = dateStr.substring(8, 10); + const noteTitle = await getJournalNoteTitle( + rootNote, "day", dayjs(dateStr), parseInt(dayNumber) + ); sql.transactional(() => { - dateNote = createNote(monthNote, noteTitle); + dateNote = createNote(dateParentNote as BNote, noteTitle); - attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10)); + attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substring(0, 10)); const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate"); @@ -205,43 +495,17 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote { } function getTodayNote(rootNote: BNote | null = null) { - return getDayNote(dateUtils.localNowDate(), rootNote); -} - -function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) { - const day = date.getDay(); - let diff; - - if (startOfTheWeek === "monday") { - diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday - } else if (startOfTheWeek === "sunday") { - diff = date.getDate() - day; - } else { - throw new Error(`Unrecognized start of the week ${startOfTheWeek}`); - } - - return new Date(date.setDate(diff)); -} - -interface WeekNoteOpts { - startOfTheWeek?: StartOfWeek; -} - -function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote: BNote | null = null) { - const startOfTheWeek = options.startOfTheWeek || "monday"; - - const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek); - - dateStr = dateUtils.utcDateTimeStr(dateObj); - - return getDayNote(dateStr, rootNote); + return getDayNote(dayjs().format("YYYY-MM-DD"), rootNote); } export default { getRootCalendarNote, getYearNote, + getQuarterNote, getMonthNote, getWeekNote, + getWeekFirstDayNote, getDayNote, - getTodayNote + getTodayNote, + getJournalNoteTitle }; diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 14967a6d6..52417bbb2 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -143,6 +143,8 @@ const defaultOptions: DefaultOption[] = [ { name: "locale", value: "en", isSynced: true }, { name: "formattingLocale", value: "en", isSynced: true }, { name: "firstDayOfWeek", value: "1", isSynced: true }, + { name: "firstWeekOfYear", value: "0", isSynced: true }, + { name: "minDaysInFirstWeek", value: "4", isSynced: true }, { name: "languages", value: "[]", isSynced: true }, // Code block configuration diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index cdd9184fb..07b89ebe2 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -85,6 +85,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions = {};; // Dictionary for storing window ID and its visibility status +const windowVisibilityMap: Record = {};; // Dictionary for storing window ID and its visibility status function getTrayIconPath() { let name: string; @@ -75,7 +76,7 @@ function updateWindowVisibilityMap(allWindows: BrowserWindow[]) { const currentWindowIds: number[] = allWindows.map(window => window.id); // Deleting closed windows from windowVisibilityMap - for (const [id, visibility] of Object.entries(windowVisibilityMap)) { + for (const [id, _] of Object.entries(windowVisibilityMap)) { const windowId = Number(id); if (!currentWindowIds.includes(windowId)) { delete windowVisibilityMap[windowId]; @@ -133,7 +134,7 @@ function updateTrayMenu() { const parentNote = becca.getNoteOrThrow("_lbBookmarks"); const menuItems: Electron.MenuItemConstructorOptions[] = []; - for (const bookmarkNote of parentNote?.children) { + for (const bookmarkNote of parentNote?.children ?? []) { if (bookmarkNote.isLabelTruthy("bookmarkFolder")) { menuItems.push({ label: bookmarkNote.title, @@ -194,7 +195,7 @@ function updateTrayMenu() { for (const idStr in windowVisibilityMap) { const id = parseInt(idStr, 10); // Get the ID of the window and make sure it is a number - const isVisible = windowVisibilityMap[id]; + const isVisible = windowVisibilityMap[id]; const win = allWindows.find(w => w.id === id); if (!win) { continue; @@ -214,10 +215,10 @@ function updateTrayMenu() { } }); } - + const contextMenu = Menu.buildFromTemplate([ - ...windowVisibilityMenuItems, + ...windowVisibilityMenuItems, { type: "separator" }, { label: t("tray.open_new_window"), @@ -235,7 +236,7 @@ function updateTrayMenu() { label: t("tray.today"), type: "normal", icon: getIconPath("today"), - click: cls.wrap(() => openInSameTab(date_notes.getTodayNote())) + click: cls.wrap(async () => openInSameTab(await date_notes.getTodayNote())) }, { label: t("tray.bookmarks"), @@ -268,7 +269,7 @@ function updateTrayMenu() { function changeVisibility() { const lastFocusedWindow = windowService.getLastFocusedWindow(); - + if (!lastFocusedWindow) { return; } diff --git a/translations/cn/server.json b/translations/cn/server.json index 8d344d267..330853c0e 100644 --- a/translations/cn/server.json +++ b/translations/cn/server.json @@ -174,6 +174,7 @@ "saturday": "周六", "sunday": "周日" }, + "weekdayNumber": "第 {weekNumber} 周", "months": { "january": "一月", "february": "二月", @@ -188,6 +189,7 @@ "november": "十一月", "december": "十二月" }, + "quarterNumber": "第 {quarterNumber} 季度", "special_notes": { "search_prefix": "搜索:" }, diff --git a/translations/en/server.json b/translations/en/server.json index e987b4148..259c099d0 100644 --- a/translations/en/server.json +++ b/translations/en/server.json @@ -174,6 +174,7 @@ "saturday": "Saturday", "sunday": "Sunday" }, + "weekdayNumber": "Week {weekNumber}", "months": { "january": "January", "february": "February", @@ -188,6 +189,7 @@ "november": "November", "december": "December" }, + "quarterNumber": "Quarter {quarterNumber}", "special_notes": { "search_prefix": "Search:" },