Merge pull request #1579 from TriliumNext/calendar

Add week note and quarter note support
This commit is contained in:
Elian Doran 2025-04-10 22:31:01 +03:00 committed by GitHub
commit a92b040958
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1273 additions and 357 deletions

View File

@ -14,7 +14,8 @@ UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'
'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', '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', 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
@ -31,7 +32,8 @@ UPDATE attributes SET name = 'name' WHERE type = 'relation'
'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', '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', 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',

View File

@ -1,6 +1,6 @@
{ {
"formatVersion": 2, "formatVersion": 2,
"appVersion": "0.92.6", "appVersion": "0.92.7",
"files": [ "files": [
{ {
"isClone": false, "isClone": false,
@ -3511,58 +3511,51 @@
"position": 20 "position": 20
}, },
{ {
"type": "label", "type": "relation",
"name": "shareAlias", "name": "internalLink",
"value": "search", "value": "OR8WJ7Iz9K4U",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-search-alt-2",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "xYmIYSP6wE3F", "value": "wX4HbRucYSDD",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "YtSN43OrfzaA", "value": "ivYnonVFBxbQ",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 50
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "OR8WJ7Iz9K4U", "value": "xYmIYSP6wE3F",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 60
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "9sRHySam5fXb", "value": "YtSN43OrfzaA",
"isInheritable": false, "isInheritable": false,
"position": 70 "position": 70
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "m523cpzocqaD", "value": "9sRHySam5fXb",
"isInheritable": false, "isInheritable": false,
"position": 80 "position": 80
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "wX4HbRucYSDD", "value": "m523cpzocqaD",
"isInheritable": false, "isInheritable": false,
"position": 90 "position": 90
}, },
@ -3590,16 +3583,23 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "ivYnonVFBxbQ", "value": "oPVyFC7WL2Lp",
"isInheritable": false, "isInheritable": false,
"position": 130 "position": 130
}, },
{ {
"type": "relation", "type": "label",
"name": "internalLink", "name": "shareAlias",
"value": "oPVyFC7WL2Lp", "value": "search",
"isInheritable": false, "isInheritable": false,
"position": 140 "position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-search-alt-2",
"isInheritable": false,
"position": 30
} }
], ],
"format": "markdown", "format": "markdown",
@ -3852,16 +3852,16 @@
"mime": "text/html", "mime": "text/html",
"attributes": [ "attributes": [
{ {
"type": "label", "type": "relation",
"name": "iconClass", "name": "internalLink",
"value": "bx bx-search-alt-2", "value": "MI26XDLSAlCD",
"isInheritable": false, "isInheritable": false,
"position": 10 "position": 10
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "8YBEPzcpUgxw", "value": "iPIMuisry3hd",
"isInheritable": false, "isInheritable": false,
"position": 20 "position": 20
}, },
@ -3875,16 +3875,16 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "iPIMuisry3hd", "value": "8YBEPzcpUgxw",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "label",
"name": "internalLink", "name": "iconClass",
"value": "MI26XDLSAlCD", "value": "bx bx-search-alt-2",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 10
} }
], ],
"format": "markdown", "format": "markdown",

View File

@ -13,10 +13,14 @@ This pattern works well also because of [Cloning Notes](../../Basic%20Concepts%2
![](Day%20Notes_image.png) ![](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). 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 ## Templates
Trilium provides [template](../Templates.md) functionality, and it could be used together with day notes. 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): You can define one of the following relations on the root of the journal (identified by `#calendarRoot` label):
* yearTemplate * yearTemplate
* quarterTemplate (if `#enableQuarterNotes` is set)
* monthTemplate * monthTemplate
* weekTemplate (if `#enableWeekNotes` is set)
* dateTemplate * 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. 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" ### Date pattern
* `{dayInMonthPadded}: {weekDay3}` day notes are named e.g. "24: Mon"
* `{dayInMonthPadded}: {weekDay2}` day notes are named e.g. "24: Mo" 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} - {weekDay}` day notes are named e.g. "2020-12-24 - Monday"
* `{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. * `{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) * `{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 * `{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`) * `{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. * `{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}` 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 ## 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. 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). 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.

View File

@ -4680,7 +4680,7 @@ otherwise (by e.g. createLink())
<h4 class="name" id="getWeekNote"><span class="type-signature"></span>getWeekNote<span class="signature">(date)</span><span class="type-signature"> &rarr; {Promise.&lt;<a href="FNote.html">FNote</a>>}</span></h4> <h4 class="name" id="getWeekFirstDayNote"><span class="type-signature"></span>getWeekFirstDayNote<span class="signature">(date)</span><span class="type-signature"> &rarr; {Promise.&lt;<a href="FNote.html">FNote</a>>}</span></h4>

View File

@ -563,7 +563,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @param {string} date - e.g. "2019-04-29" * @param {string} date - e.g. "2019-04-29"
* @returns {Promise&lt;FNote>} * @returns {Promise&lt;FNote>}
*/ */
this.getWeekNote = dateNotesService.getWeekNote; this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote;
/** /**
* Returns month-note. If it doesn't exist, it is automatically created. * Returns month-note. If it doesn't exist, it is automatically created.

View File

@ -694,7 +694,7 @@ paths:
/calendar/weeks/{date}: /calendar/weeks/{date}:
get: get:
description: returns a week note for a given date. Gets created if doesn't exist. description: returns a week note for a given date. Gets created if doesn't exist.
operationId: getWeekNote operationId: getWeekFirstDayNote
parameters: parameters:
- name: date - name: date
in: path in: path

View File

@ -5,59 +5,72 @@ import mappers from "./mappers.js";
import type { Router } from "express"; import type { Router } from "express";
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); 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 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.`); const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date: string) { function isValidDate(date: string) {
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) { return /[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date) && !!Date.parse(date);
return false;
}
return !!Date.parse(date);
} }
function register(router: Router) { 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; const { date } = req.params;
if (!isValidDate(date)) { if (!isValidDate(date)) {
throw getDateInvalidError(date); throw getDateInvalidError(date);
} }
const note = specialNotesService.getInboxNote(date); const note = await dateNotesService.getDayNote(date);
res.json(mappers.mapNoteToPojo(note)); 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; const { date } = req.params;
if (!isValidDate(date)) { if (!isValidDate(date)) {
throw getDateInvalidError(date); throw getDateInvalidError(date);
} }
const note = dateNotesService.getDayNote(date); const note = await dateNotesService.getWeekFirstDayNote(date);
res.json(mappers.mapNoteToPojo(note)); res.json(mappers.mapNoteToPojo(note));
}); });
eu.route(router, "get", "/etapi/calendar/weeks/:date", (req, res, next) => { eu.route(router, "get", "/etapi/calendar/weeks/:week", async (req, res, next) => {
const { date } = req.params; const { week } = req.params;
if (!isValidDate(date)) { if (!/[0-9]{4}-W[0-9]{2}/.test(week)) {
throw getDateInvalidError(date); throw getWeekInvalidError(week);
}
const note = await dateNotesService.getWeekNote(week);
if (!note) {
throw getWeekNotFoundError(week);
} }
const note = dateNotesService.getWeekNote(date);
res.json(mappers.mapNoteToPojo(note)); 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; const { month } = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) { if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
throw getMonthInvalidError(month); throw getMonthInvalidError(month);
} }
const note = dateNotesService.getMonthNote(month); const note = await dateNotesService.getMonthNote(month);
res.json(mappers.mapNoteToPojo(note)); res.json(mappers.mapNoteToPojo(note));
}); });

View File

@ -1,6 +1,6 @@
{ {
"formatVersion": 2, "formatVersion": 2,
"appVersion": "0.92.6", "appVersion": "0.92.7",
"files": [ "files": [
{ {
"isClone": false, "isClone": false,
@ -3511,58 +3511,51 @@
"position": 20 "position": 20
}, },
{ {
"type": "label", "type": "relation",
"name": "shareAlias", "name": "internalLink",
"value": "search", "value": "OR8WJ7Iz9K4U",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-search-alt-2",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "xYmIYSP6wE3F", "value": "wX4HbRucYSDD",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "YtSN43OrfzaA", "value": "ivYnonVFBxbQ",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 50
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "OR8WJ7Iz9K4U", "value": "xYmIYSP6wE3F",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 60
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "9sRHySam5fXb", "value": "YtSN43OrfzaA",
"isInheritable": false, "isInheritable": false,
"position": 70 "position": 70
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "m523cpzocqaD", "value": "9sRHySam5fXb",
"isInheritable": false, "isInheritable": false,
"position": 80 "position": 80
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "wX4HbRucYSDD", "value": "m523cpzocqaD",
"isInheritable": false, "isInheritable": false,
"position": 90 "position": 90
}, },
@ -3590,16 +3583,23 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "ivYnonVFBxbQ", "value": "oPVyFC7WL2Lp",
"isInheritable": false, "isInheritable": false,
"position": 130 "position": 130
}, },
{ {
"type": "relation", "type": "label",
"name": "internalLink", "name": "shareAlias",
"value": "oPVyFC7WL2Lp", "value": "search",
"isInheritable": false, "isInheritable": false,
"position": 140 "position": 20
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-search-alt-2",
"isInheritable": false,
"position": 30
} }
], ],
"format": "html", "format": "html",
@ -3852,16 +3852,16 @@
"mime": "text/html", "mime": "text/html",
"attributes": [ "attributes": [
{ {
"type": "label", "type": "relation",
"name": "iconClass", "name": "internalLink",
"value": "bx bx-search-alt-2", "value": "MI26XDLSAlCD",
"isInheritable": false, "isInheritable": false,
"position": 10 "position": 10
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "8YBEPzcpUgxw", "value": "iPIMuisry3hd",
"isInheritable": false, "isInheritable": false,
"position": 20 "position": 20
}, },
@ -3875,16 +3875,16 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "iPIMuisry3hd", "value": "8YBEPzcpUgxw",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "label",
"name": "internalLink", "name": "iconClass",
"value": "MI26XDLSAlCD", "value": "bx bx-search-alt-2",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 10
} }
], ],
"format": "html", "format": "html",

View File

@ -35,12 +35,19 @@
<img src="Day Notes_image.png"> <img src="Day Notes_image.png">
</p> </p>
<p>You can see the structure of day notes appearing under "Journal" note <p>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" - there's a note for the whole year 2025, under it, you have "03 - March"
which then contains "18 - Monday". This is our "day note" which contains 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 some text in its content and also has some child notes (some of them are
from <a href="#root/_help_xYjQUYhpbUEW">Task manager</a>).</p> from <a href="#root/_help_xYjQUYhpbUEW">Task manager</a>).</p>
<p>You can also notice how this day note has <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> "weight" <p>You can also notice how this day note has <a href="#root/_help_OFXdgB2nNk1F">promoted attribute</a> "weight"
where you can track your daily weight. This data is then used in <a href="#root/_help_R7abl2fc6Mxi">Weight tracker</a>.</p> where you can track your daily weight. This data is then used in <a href="#root/_help_R7abl2fc6Mxi">Weight tracker</a>.</p>
<h2>Week Note and Quarter Note</h2>
<p>Week and quarter notes are disabled by default, since it might be too
much for some people. To enable them, you need to set <code>#enableWeekNotes</code> and <code>#enableQuarterNotes</code> attributes
on the root calendar note, which is identified by <code>#calendarRoot</code> 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.</p>
<h2>Templates</h2> <h2>Templates</h2>
<p>Trilium provides <a href="#root/_help_KC1HB96bqqHX">template</a> functionality, <p>Trilium provides <a href="#root/_help_KC1HB96bqqHX">template</a> functionality,
and it could be used together with day notes.</p> and it could be used together with day notes.</p>
@ -48,36 +55,69 @@
(identified by <code>#calendarRoot</code> label):</p> (identified by <code>#calendarRoot</code> label):</p>
<ul> <ul>
<li>yearTemplate</li> <li>yearTemplate</li>
<li>quarterTemplate (if <code>#enableQuarterNotes</code> is set)</li>
<li>monthTemplate</li> <li>monthTemplate</li>
<li>weekTemplate (if <code>#enableWeekNotes</code> is set)</li>
<li>dateTemplate</li> <li>dateTemplate</li>
</ul> </ul>
<p>All of these are relations. When Trilium creates a new note for year or <p>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 <code>~template</code> relation month or date, it will take a look at the root and attach a corresponding <code>~template</code> relation
to the newly created role. Using this, you can e.g. create your daily template to the newly created role. Using this, you can e.g. create your daily template
with e.g. checkboxes for daily routine etc.</p> with e.g. checkboxes for daily routine etc.</p>
<h2>Date pattern</h2> <h2>Naming pattern</h2>
<p>You can customize the title of generated journal notes by defining a <code>#datePattern</code>, <code>#weekPattern</code>, <code>#monthPattern</code>, <code>#quarterPattern</code> and <code>#yearPattern</code> attribute
on a root calendar note (identified by <code>#calendarRoot</code> label).
The naming pattern replacements follow a level-up compatibility - each
level can use replacements from itself and all levels above it. For example, <code>#monthPattern</code> can
use month, quarter and year replacements, while <code>#weekPattern</code> can
use week, month, quarter and year replacements. But it is not possible
to use week replacements in <code>#monthPattern</code>.</p>
<h3>Date pattern</h3>
<p>It's possible to customize the title of generated date notes by defining <p>It's possible to customize the title of generated date notes by defining
a <code>#datePattern</code> label on a root calendar note (identified by <code>#calendarRoot</code> label). a <code>#datePattern</code> attribute on a root calendar note (identified
Following are possible values:</p> by <code>#calendarRoot</code> label). Following are possible values:</p>
<ul> <ul>
<li><code>{dayInMonthPadded} - {weekDay}</code> day notes are named e.g. "24 <li><code>{isoDate}</code> results in an ISO 8061 formatted date (e.g. "2025-03-09"
- Monday"</li> for March 9, 2025)</li>
<li><code>{dayInMonthPadded}: {weekDay3}</code> day notes are named e.g. "24: <li><code>{dateNumber}</code> results in a number like <code>9</code> for the
Mon"</li> 9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{dayInMonthPadded}: {weekDay2}</code> day notes are named e.g. "24: <li><code>{dateNumberPadded}</code> results in a number like <code>09</code> for
Mo"</li> the 9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{isoDate} - {weekDay}</code> day notes are named e.g. "2020-12-24
- Monday"</li>
<li><code>{ordinal}</code> is replaced with the ordinal date (e.g. 1st, 2nd, <li><code>{ordinal}</code> is replaced with the ordinal date (e.g. 1st, 2nd,
3rd) etc.</li> 3rd) etc.</li>
<li><code>{weekDay}</code> results in the full day name (e.g. <code>Monday</code>)</li>
<li><code>{weekDay3}</code> is replaced with the first 3 letters of the day,
e.g. Mon, Tue, etc.</li>
<li><code>{weekDay2}</code> is replaced with the first 2 letters of the day,
e.g. Mo, Tu, etc.</li>
</ul> </ul>
<h2>Month pattern</h2> <p>The default is <code>{dateNumberPadded} - {weekDay}</code>
</p>
<h3>Week pattern</h3>
<p>It is also possible to customize the title of generated week notes through
the <code>#weekPattern</code> attribute on the root calendar note. The options
are:</p>
<ul>
<li><code>{weekNumber}</code> results in a number like <code>9</code> for the
9th week of the year, <code>11</code> for the 11th week of the year</li>
<li><code>{weekNumberPadded}</code> results in a number like <code>09</code> for
the 9th week of the year, <code>11</code> for the 11th week of the year</li>
<li><code>{shortWeek}</code> results in a short week string like <code>W9</code> for
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
<li><code>{shortWeek3}</code> results in a short week string like <code>W09</code> for
the 9th week of the year, <code>W11</code> for the 11th week of the year</li>
</ul>
<p>The default is <code>Week {weekNumber}</code>
</p>
<h3>Month pattern</h3>
<p>It is also possible to customize the title of generated month notes through <p>It is also possible to customize the title of generated month notes through
the <code>#monthPattern</code> attribute, much like <code>#datePattern</code>. the <code>#monthPattern</code> attribute on the root calendar note. The options
The options are:</p> are:</p>
<ul> <ul>
<li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g. "2025-03" <li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g. "2025-03"
for March 2025)</li> for March 2025)</li>
<li><code>{monthNumber}</code> results in a number like <code>9</code> for September,
and <code>11</code> for November</li>
<li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for <li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for
September, and <code>11</code> for November</li> September, and <code>11</code> for November</li>
<li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li> <li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li>
@ -88,14 +128,37 @@
</ul> </ul>
<p>The default is <code>{monthNumberPadded} - {month}</code> <p>The default is <code>{monthNumberPadded} - {month}</code>
</p> </p>
<h3>Quarter pattern</h3>
<p>It is also possible to customize the title of generated quarter notes
through the <code>#quarterPattern</code> attribute on the root calendar note.
The options are:</p>
<ul>
<li><code>{quarterNumber}</code> results in a number like <code>1</code> for
the 1st quarter of the year</li>
<li><code>{shortQuarter}</code> results in a short quarter string like <code>Q1</code> for
the 1st quarter of the year</li>
</ul>
<p>The default is <code>Quarter {quarterNumber}</code>
</p>
<h3>Year pattern</h3>
<p>It is also possible to customize the title of generated year notes through
the <code>#yearPattern</code> attribute on the root calendar note. The options
are:</p>
<ul>
<li><code>{year}</code> results in the full year (e.g. <code>2025</code>)</li>
</ul>
<p>The default is <code>{year}</code>
</p>
<h2>Implementation</h2> <h2>Implementation</h2>
<p>Trilium has some special support for day notes in the form of <a href="https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html">backend Script API</a> - <p>Trilium has some special support for day notes in the form of <a href="https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html">backend Script API</a> -
see e.g. getDayNote() function.</p> see e.g. getDayNote() function.</p>
<p>Day (and year, month) notes are created with a label - e.g. <code>#dateNote="2018-08-16"</code> this <p>Day (and year, month) notes are created with a label - e.g. <code>#dateNote="2025-03-09"</code> this
can then be used by other scripts to add new notes to day note etc.</p> can then be used by other scripts to add new notes to day note etc.</p>
<p>Journal also has relation <code>child:child:child:template=Day template</code> (see <p>Journal also has relation <code>child:child:child:template=Day template</code> (see
[[attribute inheritance]]) which effectively adds [[template]] to day notes [[attribute inheritance]]) which effectively adds [[template]] to day notes
(grand-grand-grand children of Journal).</p> (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.</p>
</div> </div>
</div> </div>
</body> </body>

View File

@ -21,19 +21,18 @@
<h2>Alternatives</h2> <h2>Alternatives</h2>
<ul> <ul>
<li>Pressing Ctrl+F while in a browser while not focused in a&nbsp;<a class="reference-link" <li>Pressing Ctrl+F while in a browser while not focused in a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;or href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;or a&nbsp;<a class="reference-link"
a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note will trigger the browser's
will trigger the browser's native search. This will also find text that native search. This will also find text that is part of Trilium's UI.</li>
is part of Trilium's UI.</li> <li>Pressing Ctrl+F in a&nbsp;<a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;note
<li>Pressing Ctrl+F in a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a>&nbsp;note will reveal&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>'s
will reveal&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/1YeN2MzFUluU/_help_MI26XDLSAlCD">CKEditor</a>'s
search functionality.</li> search functionality.</li>
</ul> </ul>
<h2>Accessing the search</h2> <h2>Accessing the search</h2>
<ul> <ul>
<li>On desktop, press<kbd>Ctrl</kbd> + <kbd>F</kbd> <li>On desktop, press<kbd>Ctrl</kbd> + <kbd>F</kbd>
</li> </li>
<li>From the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a>, <li>From the&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>,
look for the context menu and select <em>Search in note</em>.</li> look for the context menu and select <em>Search in note</em>.</li>
</ul> </ul>
<h2>Interaction</h2> <h2>Interaction</h2>

View File

@ -24,10 +24,10 @@
results as sub-items.</p> results as sub-items.</p>
<h2>Accessing the search</h2> <h2>Accessing the search</h2>
<ul> <ul>
<li>From the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>, <li>From the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>,
look for the dedicated search button.</li> look for the dedicated search button.</li>
<li>To limit the search to a note and its children, select <em>Search from subtree</em> from <li>To limit the search to a note and its children, select <em>Search from subtree</em> from
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/oPVyFC7WL2Lp/_help_YtSN43OrfzaA">Note tree contextual menu</a>&nbsp;or the&nbsp;<a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note tree contextual menu</a>&nbsp;or
press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd>.</li> press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd>.</li>
</ul> </ul>
<h2>Interaction</h2> <h2>Interaction</h2>
@ -43,8 +43,8 @@
</li> </li>
<li>To limit the search to a note and its sub-children, set a note in <em>Ancestor</em>. <li>To limit the search to a note and its sub-children, set a note in <em>Ancestor</em>.
<ol> <ol>
<li>This value is also pre-filled if the search is triggered from a <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_OR8WJ7Iz9K4U">hoisted note</a> or <li>This value is also pre-filled if the search is triggered from a <a href="#root/_help_OR8WJ7Iz9K4U">hoisted note</a> or
a <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_9sRHySam5fXb">workspace</a>.</li> a <a href="#root/_help_9sRHySam5fXb">workspace</a>.</li>
<li>To search the entire database, keep the value empty.</li> <li>To search the entire database, keep the value empty.</li>
</ol> </ol>
</li> </li>
@ -58,7 +58,7 @@
<li>The <em>Search &amp; Execute actions</em> button is only relevant if at <li>The <em>Search &amp; Execute actions</em> button is only relevant if at
least one action has been added (as described in the section below).</li> least one action has been added (as described in the section below).</li>
<li>The <em>Save to note</em> will create a new note with the search configuration. <li>The <em>Save to note</em> will create a new note with the search configuration.
For more information, see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>.</li> For more information, see&nbsp;<a class="reference-link" href="#root/_help_m523cpzocqaD">Saved Search</a>.</li>
</ol> </ol>
<h2>Search options</h2> <h2>Search options</h2>
<p>Click on which search option to apply from the Add search option section.</p> <p>Click on which search option to apply from the Add search option section.</p>
@ -71,7 +71,7 @@
<ol> <ol>
<li>Search script <li>Search script
<ol> <ol>
<li>This feature allows writing a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note <li>This feature allows writing a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note
that will handle the search on its own.</li> that will handle the search on its own.</li>
</ol> </ol>
</li> </li>
@ -79,12 +79,12 @@
<ol> <ol>
<li>The search will not look into the content of the notes, but it will still <li>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).</li> look into note titles and attributes, relations (based on the search query).</li>
<li>This method can speed up the search considerably for large <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_wX4HbRucYSDD">databases</a>.</li> <li>This method can speed up the search considerably for large <a href="#root/_help_wX4HbRucYSDD">databases</a>.</li>
</ol> </ol>
</li> </li>
<li>Include archived <li>Include archived
<ol> <ol>
<li><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_MKmLg5x6xkor">Archived Notes</a>&nbsp;will <li><a class="reference-link" href="#root/_help_MKmLg5x6xkor">Archived Notes</a>&nbsp;will
also be included in the results, whereas otherwise they would be ignored.</li> also be included in the results, whereas otherwise they would be ignored.</li>
</ol> </ol>
</li> </li>
@ -107,7 +107,7 @@
<ol> <ol>
<li>This will print additional information in the server log (see&nbsp; <li>This will print additional information in the server log (see&nbsp;
<a <a
class="reference-link" href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_qzNzp9LYQyPT">Error logs</a>), regarding how the search expression was parsed.</li> class="reference-link" href="#root/_help_qzNzp9LYQyPT">Error logs</a>), regarding how the search expression was parsed.</li>
<li>This function is especially useful after understanding the search functionality <li>This function is especially useful after understanding the search functionality
in detail, in order to determine why a complex search query is not working in detail, in order to determine why a complex search query is not working
as expected.</li> as expected.</li>
@ -122,10 +122,9 @@
action multiple times (i.e. in order to be able to apply multiple labels action multiple times (i.e. in order to be able to apply multiple labels
to notes).</li> to notes).</li>
<li>The actions given are the same as the ones in&nbsp;<a class="reference-link" <li>The actions given are the same as the ones in&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_ivYnonVFBxbQ">Bulk Actions</a>, href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a>, which is an alternative
which is an alternative for operating directly with notes within the&nbsp; for operating directly with notes within the&nbsp;<a class="reference-link"
<a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
<li>After defining the actions, first press <em>Search</em> to check the matched <li>After defining the actions, first press <em>Search</em> to check the matched
notes and then press <em>Search &amp; Execute actions</em> to trigger the notes and then press <em>Search &amp; Execute actions</em> to trigger the
actions.</li> actions.</li>

View File

@ -22,14 +22,22 @@ async function getDayNote(date: string) {
return await froca.getNote(note.noteId); return await froca.getNote(note.noteId);
} }
async function getWeekNote(date: string) { async function getWeekFirstDayNote(date: string) {
const note = await server.get<FNoteRow>(`special-notes/weeks/${date}`, "date-note"); const note = await server.get<FNoteRow>(`special-notes/week-first-day/${date}`, "date-note");
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId); return await froca.getNote(note.noteId);
} }
async function getWeekNote(week: string) {
const note = await server.get<FNoteRow>(`special-notes/weeks/${week}`, "date-note");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note?.noteId);
}
async function getMonthNote(month: string) { async function getMonthNote(month: string) {
const note = await server.get<FNoteRow>(`special-notes/months/${month}`, "date-note"); const note = await server.get<FNoteRow>(`special-notes/months/${month}`, "date-note");
@ -38,6 +46,14 @@ async function getMonthNote(month: string) {
return await froca.getNote(note.noteId); return await froca.getNote(note.noteId);
} }
async function getQuarterNote(quarter: string) {
const note = await server.get<FNoteRow>(`special-notes/quarters/${quarter}`, "date-note");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
async function getYearNote(year: string) { async function getYearNote(year: string) {
const note = await server.get<FNoteRow>(`special-notes/years/${year}`, "date-note"); const note = await server.get<FNoteRow>(`special-notes/years/${year}`, "date-note");
@ -66,7 +82,9 @@ export default {
getInboxNote, getInboxNote,
getTodayNote, getTodayNote,
getDayNote, getDayNote,
getWeekFirstDayNote,
getWeekNote, getWeekNote,
getQuarterNote,
getMonthNote, getMonthNote,
getYearNote, getYearNote,
createSqlConsole, createSqlConsole,

View File

@ -363,6 +363,14 @@ interface Api {
* *
* @param date - e.g. "2019-04-29" * @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; getWeekNote: typeof dateNotesService.getWeekNote;
/** /**
@ -372,6 +380,14 @@ interface Api {
*/ */
getMonthNote: typeof dateNotesService.getMonthNote; 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. * 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.getTodayNote = dateNotesService.getTodayNote;
this.getDayNote = dateNotesService.getDayNote; this.getDayNote = dateNotesService.getDayNote;
this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote;
this.getWeekNote = dateNotesService.getWeekNote; this.getWeekNote = dateNotesService.getWeekNote;
this.getMonthNote = dateNotesService.getMonthNote; this.getMonthNote = dateNotesService.getMonthNote;
this.getQuarterNote = dateNotesService.getQuarterNote;
this.getYearNote = dateNotesService.getYearNote; this.getYearNote = dateNotesService.getYearNote;
this.setHoistedNoteId = (noteId) => { this.setHoistedNoteId = (noteId) => {

View File

@ -1,5 +1,4 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import dateNoteService from "../../services/date_notes.js"; import dateNoteService from "../../services/date_notes.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import appContext from "../../components/app_context.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 options from "../../services/options.js";
import { Dropdown } from "bootstrap"; import { Dropdown } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; 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"; import "../../../stylesheets/calendar.css";
dayjs.extend(utc);
dayjs.extend(isSameOrAfter);
const MONTHS = [ const MONTHS = [
t("calendar.january"), t("calendar.january"),
t("calendar.febuary"), t("calendar.febuary"),
@ -29,7 +35,7 @@ const DROPDOWN_TPL = `
<div class="calendar-dropdown-widget"> <div class="calendar-dropdown-widget">
<style> <style>
.calendar-dropdown-widget { .calendar-dropdown-widget {
width: 350px; width: 400px;
} }
</style> </style>
@ -59,9 +65,7 @@ const DROPDOWN_TPL = `
</div> </div>
</div> </div>
<div class="calendar-week"> <div class="calendar-week"></div>
</div>
<div class="calendar-body" data-calendar-area="month"></div> <div class="calendar-body" data-calendar-area="month"></div>
</div>`; </div>`;
@ -71,6 +75,11 @@ interface DateNotesForMonth {
[date: string]: string; [date: string]: string;
} }
interface WeekCalculationOptions {
firstWeekType: number;
minDaysInFirstWeek: number;
}
export default class CalendarWidget extends RightDropdownButtonWidget { export default class CalendarWidget extends RightDropdownButtonWidget {
private $month!: JQuery<HTMLElement>; private $month!: JQuery<HTMLElement>;
private $weekHeader!: JQuery<HTMLElement>; private $weekHeader!: JQuery<HTMLElement>;
@ -82,9 +91,12 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
private $previousYear!: JQuery<HTMLElement>; private $previousYear!: JQuery<HTMLElement>;
private monthDropdown!: Dropdown; private monthDropdown!: Dropdown;
private firstDayOfWeek!: number; private firstDayOfWeek!: number;
private activeDate: Date | null = null; private weekCalculationOptions!: WeekCalculationOptions;
private todaysDate!: Date; private activeDate: Dayjs | null = null;
private date!: Date; private todaysDate!: Dayjs;
private date!: Dayjs;
private weekNoteEnable: boolean = false;
private weekNotes: string[] = [];
constructor(title: string = "", icon: string = "") { constructor(title: string = "", icon: string = "") {
super(title, icon, DROPDOWN_TPL); super(title, icon, DROPDOWN_TPL);
@ -97,6 +109,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.$weekHeader = this.$dropdownContent.find(".calendar-week"); this.$weekHeader = this.$dropdownContent.find(".calendar-week");
this.manageFirstDayOfWeek(); this.manageFirstDayOfWeek();
this.initWeekCalculation();
// Month navigation // Month navigation
this.$monthSelect = this.$dropdownContent.find('[data-calendar-input="month"]'); 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 target = e.target as HTMLElement;
const value = target.dataset.value; const value = target.dataset.value;
if (value) { if (value) {
this.date.setMonth(parseInt(value)); this.date = this.date.month(parseInt(value));
this.createMonth(); this.createMonth();
} }
}); });
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]'); this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
this.$next.on("click", () => { this.$next.on("click", () => {
this.date.setMonth(this.date.getMonth() + 1); this.date = this.date.add(1, 'month');
this.createMonth(); this.createMonth();
}); });
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]'); this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
this.$previous.on("click", () => { this.$previous.on("click", () => {
this.date.setMonth(this.date.getMonth() - 1); this.date = this.date.subtract(1, 'month');
this.createMonth(); this.createMonth();
}); });
@ -128,17 +141,17 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]'); this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]');
this.$yearSelect.on("input", (e) => { this.$yearSelect.on("input", (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
this.date.setFullYear(parseInt(target.value)); this.date = this.date.year(parseInt(target.value));
this.createMonth(); this.createMonth();
}); });
this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]'); this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]');
this.$nextYear.on("click", () => { this.$nextYear.on("click", () => {
this.date.setFullYear(this.date.getFullYear() + 1); this.date = this.date.add(1, 'year');
this.createMonth(); this.createMonth();
}); });
this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]'); this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]');
this.$previousYear.on("click", () => { this.$previousYear.on("click", () => {
this.date.setFullYear(this.date.getFullYear() - 1); this.date = this.date.subtract(1, 'year');
this.createMonth(); this.createMonth();
}); });
@ -159,6 +172,27 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
ev.stopPropagation(); 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 // Handle click events for the entire calendar widget
this.$dropdownContent.on("click", (e) => { this.$dropdownContent.on("click", (e) => {
const $target = $(e.target); const $target = $(e.target);
@ -177,57 +211,139 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
}); });
} }
private async getWeekNoteEnable() {
const noteId = await server.get<string[]>(`search/${encodeURIComponent('#calendarRoot')}`);
if (noteId.length === 0) {
this.weekNoteEnable = false;
return;
}
const noteAttributes = await server.get<BAttribute[]>(`notes/${noteId}/attributes`);
for (const attribute of noteAttributes) {
if (attribute.name === 'enableWeekNote') {
this.weekNoteEnable = true;
return
}
}
this.weekNoteEnable = false;
}
manageFirstDayOfWeek() { manageFirstDayOfWeek() {
this.firstDayOfWeek = options.getInt("firstDayOfWeek") || 0; 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. // Generate the list of days of the week taking into consideration the user's selected first day of week.
let localeDaysOfWeek = [...DAYS_OF_WEEK]; let localeDaysOfWeek = [...DAYS_OF_WEEK];
const daysToBeAddedAtEnd = localeDaysOfWeek.splice(0, this.firstDayOfWeek); const daysToBeAddedAtEnd = localeDaysOfWeek.splice(0, this.firstDayOfWeek);
localeDaysOfWeek = [...localeDaysOfWeek, ...daysToBeAddedAtEnd]; localeDaysOfWeek = ['', ...localeDaysOfWeek, ...daysToBeAddedAtEnd];
this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`).join('')); this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`).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() { async dropdownShown() {
await this.getWeekNoteEnable();
this.weekNotes = await server.get<string[]>(`attribute-values/weekNote`);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null); this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
} }
init(activeDate: string | null) { init(activeDate: string | null) {
// attaching time fixes local timezone handling // attaching time fixes local timezone handling
this.activeDate = activeDate ? new Date(`${activeDate}T12:00:00`) : null; this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null;
this.todaysDate = new Date(); this.todaysDate = dayjs();
this.date = new Date((this.activeDate || this.todaysDate).getTime()); this.date = dayjs(this.activeDate || this.todaysDate).startOf('month');
this.date.setDate(1);
this.createMonth(); this.createMonth();
} }
createDay(dateNotesForMonth: DateNotesForMonth, num: number, day: number) { createDay(dateNotesForMonth: DateNotesForMonth, num: number) {
const $newDay = $("<a>").addClass("calendar-date").attr("data-calendar-date", utils.formatDateISO(this.date)); const $newDay = $("<a>").addClass("calendar-date").attr("data-calendar-date", this.date.local().format('YYYY-MM-DD'));
const $date = $("<span>").html(String(num)); const $date = $("<span>").html(String(num));
// if it's the first day of the month const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')];
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)];
if (dateNoteId) { if (dateNoteId) {
$newDay.addClass("calendar-date-exists"); $newDay.addClass("calendar-date-exists");
$newDay.attr("data-href", `#root/${dateNoteId}`); $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"); $newDay.addClass("calendar-date-active");
} }
if (this.isEqual(this.date, this.todaysDate)) { if (this.date.isSame(this.todaysDate, 'day')) {
$newDay.addClass("calendar-date-today"); $newDay.addClass("calendar-date-today");
} }
@ -235,44 +351,140 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
return $newDay; return $newDay;
} }
isEqual(a: Date, b: Date | null) { createWeekNumber(weekNumber: number) {
if ((!a && b) || (a && !b)) { const weekNoteId = this.date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0');
return false;
let $newWeekNumber;
if (this.weekNoteEnable) {
// Utilize the hover effect of calendar-date
$newWeekNumber = $("<a>").addClass("calendar-date");
if (this.weekNotes.includes(weekNoteId)) {
$newWeekNumber.addClass("calendar-date-exists");
$newWeekNumber.attr("data-href", `#root/${weekNoteId}`);
}
} else {
$newWeekNumber = $("<span>").addClass("calendar-week-number-disabled");
}
$newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId);
$newWeekNumber.append($("<span>").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() { 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}`); const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`);
this.$month.empty(); this.$month.empty();
const currentMonth = this.date.getMonth(); const firstDay = this.date.startOf('month');
while (this.date.getMonth() === currentMonth) { const firstDayOfWeek = firstDay.day();
const $day = this.createDay(dateNotesForMonth, this.date.getDate(), this.date.getDay());
// 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.$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 // while loop trips over and day is at 30/31, bring it back
this.date.setDate(1); this.date = this.date.startOf('month').subtract(1, 'month');
this.date.setMonth(this.date.getMonth() - 1);
this.$monthSelect.text(MONTHS[this.date.getMonth()]); // Add dates from next month
this.$yearSelect.val(this.date.getFullYear()); 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">) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (!loadResults.getOptionNames().includes("firstDayOfWeek")) { if (!loadResults.getOptionNames().includes("firstDayOfWeek") &&
!loadResults.getOptionNames().includes("firstWeekOfYear") &&
!loadResults.getOptionNames().includes("minDaysInFirstWeek")) {
return; return;
} }
this.manageFirstDayOfWeek(); this.manageFirstDayOfWeek();
this.initWeekCalculation();
this.createMonth(); this.createMonth();
} }
} }

View File

@ -35,6 +35,41 @@ const TPL = /*html*/`
</div> </div>
</div> </div>
<div class="option-row">
<label id="first-week-of-year-label">${t("i18n.first-week-of-the-year")}</label>
<div role="group" aria-labelledby="first-week-of-year-label">
<label class="tn-radio">
<input name="first-week-of-year" type="radio" value="0" />
${t("i18n.first-week-contains-first-day")}
</label>
<label class="tn-radio">
<input name="first-week-of-year" type="radio" value="1" />
${t("i18n.first-week-contains-first-thursday")}
</label>
<label class="tn-radio">
<input name="first-week-of-year" type="radio" value="2" />
${t("i18n.first-week-has-minimum-days")}
</label>
</div>
</div>
<div class="option-row min-days-row" style="display: none;">
<label for="min-days-in-first-week">${t("i18n.min-days-in-first-week")}</label>
<select id="min-days-in-first-week" class="form-select">
${Array.from({ length: 7 }, (_, i) => i + 1)
.map(num => `<option value="${num}">${num}</option>`)
.join('')}
</select>
</div>
<p class="form-text">${t("i18n.first-week-info")}</p>
<div class="admonition warning" role="alert">
${t("i18n.first-week-warning")}
</div>
<div class="option-row centered"> <div class="option-row centered">
<button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button> <button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
</div> </div>
@ -64,6 +99,16 @@ const TPL = /*html*/`
.locale-options-container .option-row.centered { .locale-options-container .option-row.centered {
justify-content: center; 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;
}
</style> </style>
</div> </div>
`; `;
@ -72,10 +117,13 @@ export default class LocalizationOptions extends OptionsWidget {
private $localeSelect!: JQuery<HTMLElement>; private $localeSelect!: JQuery<HTMLElement>;
private $formattingLocaleSelect!: JQuery<HTMLElement>; private $formattingLocaleSelect!: JQuery<HTMLElement>;
private $minDaysRow!: JQuery<HTMLElement>;
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$minDaysRow = this.$widget.find(".min-days-row");
this.$localeSelect = this.$widget.find(".locale-select"); this.$localeSelect = this.$widget.find(".locale-select");
this.$localeSelect.on("change", async () => { this.$localeSelect.on("change", async () => {
const newLocale = this.$localeSelect.val(); 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()); const firstDayOfWeek = String(this.$widget.find(`input[name="first-day-of-week"]:checked`).val());
this.updateOption("firstDayOfWeek", firstDayOfWeek); 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); 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.$formattingLocaleSelect.val(options.formattingLocale);
this.$widget.find(`input[name="first-day-of-week"][value="${options.firstDayOfWeek}"]`) 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);
} }
} }

View File

@ -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 { EventData, EventListener } from "../../../components/app_context.js";
import type FNote from "../../../entities/fnote.js"; import type FNote from "../../../entities/fnote.js";
import { t } from "../../../services/i18n.js"; import { t } from "../../../services/i18n.js";
@ -45,7 +45,7 @@ export default class OptionsWidget extends NoteContextAwareWidget implements Eve
$checkbox.prop("checked", optionValue === "true"); $checkbox.prop("checked", optionValue === "true");
} }
optionsLoaded(options: OptionMap) {} optionsLoaded(options: OptionMap) { }
async refresh() { async refresh() {
this.toggleInt(this.isEnabled()); this.toggleInt(this.isEnabled());

View File

@ -35,7 +35,7 @@
padding: 0 0.5rem 0.5rem 0.5rem; padding: 0 0.5rem 0.5rem 0.5rem;
} }
.calendar-dropdown-widget .calendar-header > div { .calendar-dropdown-widget .calendar-header>div {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-grow: 1; flex-grow: 1;
@ -67,7 +67,8 @@
} }
.calendar-dropdown-widget .calendar-header .dropdown-toggle::after { .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 { .calendar-dropdown-widget .calendar-week {
@ -77,10 +78,10 @@
.calendar-dropdown-widget .calendar-week span { .calendar-dropdown-widget .calendar-week span {
flex-direction: column; flex-direction: column;
flex: 0 0 14.28%; flex: 0 0 12.5%;
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
max-width: 14.28%; max-width: 12.5%;
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
text-align: center; text-align: center;
@ -92,13 +93,40 @@
flex-wrap: wrap; 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 { .calendar-dropdown-widget .calendar-date {
align-items: center; align-items: center;
color: var(--main-text-color); color: var(--main-text-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 0 0 14.28%; flex: 0 0 12.5%;
max-width: 14.28%; max-width: 12.5%;
padding: 0.4rem 0; padding: 0.4rem 0;
font-size: 120%; font-size: 120%;
} }
@ -129,3 +157,17 @@
.calendar-dropdown-widget .calendar-date:not(.calendar-date-active) { .calendar-dropdown-widget .calendar-date:not(.calendar-date-active) {
cursor: pointer; 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;
}

View File

@ -334,7 +334,6 @@ body.layout-horizontal > .horizontal {
} }
.calendar-dropdown-widget .calendar-header { .calendar-dropdown-widget .calendar-header {
padding: 8px 0 20px 0;
gap: 10px; gap: 10px;
} }

View File

@ -588,6 +588,7 @@
"sat": "六", "sat": "六",
"sun": "日", "sun": "日",
"cannot_find_day_note": "无法找到日记", "cannot_find_day_note": "无法找到日记",
"cannot_find_week_note": "无法找到周记",
"january": "一月", "january": "一月",
"febuary": "二月", "febuary": "二月",
"march": "三月", "march": "三月",
@ -1231,7 +1232,15 @@
"language": "语言", "language": "语言",
"first-day-of-the-week": "一周的第一天", "first-day-of-the-week": "一周的第一天",
"sunday": "周日", "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": "第一周包含一年的第一个周四,基于 <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a> 标准。",
"first-week-warning": "更改第一周选项可能会导致与现有周笔记重复,已创建的周笔记将不会相应更新。",
"formatting-locale": "日期和数字格式"
}, },
"backup": { "backup": {
"automatic_backup": "自动备份", "automatic_backup": "自动备份",

View File

@ -588,6 +588,7 @@
"sat": "Sat", "sat": "Sat",
"sun": "Sun", "sun": "Sun",
"cannot_find_day_note": "Cannot find day note", "cannot_find_day_note": "Cannot find day note",
"cannot_find_week_note": "Cannot find week note",
"january": "January", "january": "January",
"febuary": "February", "febuary": "February",
"march": "March", "march": "March",
@ -1242,6 +1243,13 @@
"first-day-of-the-week": "First day of the week", "first-day-of-the-week": "First day of the week",
"sunday": "Sunday", "sunday": "Sunday",
"monday": "Monday", "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 <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a> 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" "formatting-locale": "Date & number format"
}, },
"backup": { "backup": {

View File

@ -1,23 +1,21 @@
"use strict";
import type { Request } from "express"; 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 jsdom from "jsdom";
import path from "path";
import type BNote from "../../becca/entities/bnote.js"; import type BNote from "../../becca/entities/bnote.js";
import ValidationError from "../../errors/validation_error.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; const { JSDOM } = jsdom;
interface Image { interface Image {
@ -26,14 +24,14 @@ interface Image {
imageId: string; imageId: string;
} }
function addClipping(req: Request) { async function addClipping(req: Request) {
// if a note under the clipperInbox has the same 'pageUrl' attribute, // if a note under the clipperInbox has the same 'pageUrl' attribute,
// add the content to that note and clone it under today's inbox // add the content to that note and clone it under today's inbox
// otherwise just create a new note under today's inbox // otherwise just create a new note under today's inbox
const { title, content, images } = req.body; const { title, content, images } = req.body;
const clipType = "clippings"; const clipType = "clippings";
const clipperInbox = getClipperInboxNote(); const clipperInbox = await getClipperInboxNote();
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl); const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType); 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]; return clipType ? notes.find((note) => note.getOwnedLabelValue("clipType") === clipType) : notes[0];
} }
function getClipperInboxNote() { async function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel("clipperInbox"); let clipperInbox = attributeService.getNoteWithLabel("clipperInbox");
if (!clipperInbox) { if (!clipperInbox) {
clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate()); clipperInbox = await dateNoteService.getDayNote(dateUtils.localNowDate());
} }
return clipperInbox; return clipperInbox;
} }
function createNote(req: Request) { async function createNote(req: Request) {
const { content, images, labels } = req.body; const { content, images, labels } = req.body;
const clipType = htmlSanitizer.sanitize(req.body.clipType); 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 trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : "";
const title = trimmedTitle || `Clipped note from ${pageUrl}`; const title = trimmedTitle || `Clipped note from ${pageUrl}`;
const clipperInbox = getClipperInboxNote(); const clipperInbox = await getClipperInboxNote();
let note = findClippingNote(clipperInbox, pageUrl, clipType); let note = findClippingNote(clipperInbox, pageUrl, clipType);
if (!note) { if (!note) {
@ -215,9 +213,9 @@ function handshake() {
}; };
} }
function findNotesByUrl(req: Request) { async function findNotesByUrl(req: Request) {
const pageUrl = req.params.noteUrl; const pageUrl = req.params.noteUrl;
const clipperInbox = getClipperInboxNote(); const clipperInbox = await getClipperInboxNote();
const foundPage = findClippingNote(clipperInbox, pageUrl, null); const foundPage = findClippingNote(clipperInbox, pageUrl, null);
return { return {
noteId: foundPage ? foundPage.noteId : null noteId: foundPage ? foundPage.noteId : null

View File

@ -72,6 +72,8 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
"locale", "locale",
"formattingLocale", "formattingLocale",
"firstDayOfWeek", "firstDayOfWeek",
"firstWeekOfYear",
"minDaysInFirstWeek",
"languages", "languages",
"textNoteEditorType", "textNoteEditorType",
"textNoteEditorMultilineToolbar", "textNoteEditorMultilineToolbar",

View File

@ -1,11 +1,10 @@
"use strict"; import type { Request } from "express";
import imageType from "image-type"; import imageType from "image-type";
import imageService from "../../services/image.js"; import imageService from "../../services/image.js";
import noteService from "../../services/notes.js"; import noteService from "../../services/notes.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js"; import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import specialNotesService from "../../services/special_notes.js"; import specialNotesService from "../../services/special_notes.js";
import type { Request } from "express";
async function uploadImage(req: Request) { async function uploadImage(req: Request) {
const file = req.file; const file = req.file;
@ -34,7 +33,7 @@ async function uploadImage(req: Request) {
return [400, "Invalid 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, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true); 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"])) { if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) {
return [400, "Invalid 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({ const { note, branch } = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,

View File

@ -1,5 +1,3 @@
"use strict";
import dateNoteService from "../../services/date_notes.js"; import dateNoteService from "../../services/date_notes.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import cls from "../../services/cls.js"; import cls from "../../services/cls.js";
@ -15,14 +13,22 @@ function getDayNote(req: Request) {
return dateNoteService.getDayNote(req.params.date); return dateNoteService.getDayNote(req.params.date);
} }
function getWeekFirstDayNote(req: Request) {
return dateNoteService.getWeekFirstDayNote(req.params.date);
}
function getWeekNote(req: Request) { function getWeekNote(req: Request) {
return dateNoteService.getWeekNote(req.params.date); return dateNoteService.getWeekNote(req.params.week);
} }
function getMonthNote(req: Request) { function getMonthNote(req: Request) {
return dateNoteService.getMonthNote(req.params.month); return dateNoteService.getMonthNote(req.params.month);
} }
function getQuarterNote(req: Request) {
return dateNoteService.getQuarterNote(req.params.quarter);
}
function getYearNote(req: Request) { function getYearNote(req: Request) {
return dateNoteService.getYearNote(req.params.year); return dateNoteService.getYearNote(req.params.year);
} }
@ -58,8 +64,8 @@ function getDayNotesForMonth(req: Request) {
} }
} }
function saveSqlConsole(req: Request) { async function saveSqlConsole(req: Request) {
return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId); return await specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId);
} }
function createSqlConsole() { function createSqlConsole() {
@ -101,8 +107,10 @@ function createOrUpdateScriptLauncherFromApi(req: Request) {
export default { export default {
getInboxNote, getInboxNote,
getDayNote, getDayNote,
getWeekFirstDayNote,
getWeekNote, getWeekNote,
getMonthNote, getMonthNote,
getQuarterNote,
getYearNote, getYearNote,
getDayNotesForMonth, getDayNotesForMonth,
createSqlConsole, createSqlConsole,

View File

@ -1,5 +1,3 @@
"use strict";
import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js"; import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js";
import multer from "multer"; import multer from "multer";
import log from "../services/log.js"; 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/inbox/:date", specialNotesRoute.getInboxNote);
apiRoute(GET, "/api/special-notes/days/:date", specialNotesRoute.getDayNote); 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/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/years/:year", specialNotesRoute.getYearNote);
apiRoute(GET, "/api/special-notes/notes-for-month/:month", specialNotesRoute.getDayNotesForMonth); apiRoute(GET, "/api/special-notes/notes-for-month/:month", specialNotesRoute.getDayNotesForMonth);
apiRoute(PST, "/api/special-notes/sql-console", specialNotesRoute.createSqlConsole); apiRoute(PST, "/api/special-notes/sql-console", specialNotesRoute.createSqlConsole);

View File

@ -224,14 +224,14 @@ interface Api {
* @param date in YYYY-MM-DD format * @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar * @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<BNote | null>;
/** /**
* Returns today's day note. If such note doesn't exist, it is created. * 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 * @param rootNote specify calendar root note, normally leave empty to use the default calendar
*/ */
getTodayNote(rootNote?: BNote): BNote | null; getTodayNote(rootNote?: BNote): Promise<BNote | null>;
/** /**
* Returns note for the first date of the week of the given date. * 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 date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar * @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/ */
getWeekNote( getWeekFirstDayNote(date: string, rootNote: BNote): Promise<BNote | null>;
date: string,
options: { /**
// TODO: Deduplicate type with date_notes.ts once ES modules are added. * Returns week note for given date. If such a note doesn't exist, it is created.
/** either "monday" (default) or "sunday" */ *
startOfTheWeek: "monday" | "sunday"; * @param date in YYYY-MM-DD format
}, * @param rootNote - specify calendar root note, normally leave empty to use the default calendar
rootNote: BNote */
): BNote | null; getWeekNote(date: string, rootNote: BNote): Promise<BNote | null>;
/** /**
* Returns month note for given date. If such a note doesn't exist, it is created. * 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 date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar * @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<BNote | null>;
/**
* 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<BNote | null>;
/** /**
* Returns year note for given year. If such a note doesn't exist, it is created. * 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.getRootCalendarNote = dateNoteService.getRootCalendarNote;
this.getDayNote = dateNoteService.getDayNote; this.getDayNote = dateNoteService.getDayNote;
this.getTodayNote = dateNoteService.getTodayNote; this.getTodayNote = dateNoteService.getTodayNote;
this.getWeekFirstDayNote = dateNoteService.getWeekFirstDayNote;
this.getWeekNote = dateNoteService.getWeekNote; this.getWeekNote = dateNoteService.getWeekNote;
this.getMonthNote = dateNoteService.getMonthNote; this.getMonthNote = dateNoteService.getMonthNote;
this.getQuarterNote = dateNoteService.getQuarterNote;
this.getYearNote = dateNoteService.getYearNote; this.getYearNote = dateNoteService.getYearNote;
this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(parentNoteId, sortConfig.sortBy || "title", !!sortConfig.reverse, !!sortConfig.foldersFirst); 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 { export default BackendScriptApi as any as {
new (currentNote: BNote, apiParams: ApiParams): Api; new(currentNote: BNote, apiParams: ApiParams): Api;
}; };

View File

@ -36,6 +36,12 @@ export default [
{ type: "label", name: "workspaceSearchHome" }, { type: "label", name: "workspaceSearchHome" },
{ type: "label", name: "sqlConsoleHome" }, { type: "label", name: "sqlConsoleHome" },
{ type: "label", name: "datePattern" }, { 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: "pageSize" },
{ type: "label", name: "viewType" }, { type: "label", name: "viewType" },
{ type: "label", name: "mapRootNoteId" }, { type: "label", name: "mapRootNoteId" },

View File

@ -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<string, string> = {
"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<string, string> = {
"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");
});
});
});

View File

@ -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 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"; import { t } from "i18next";
dayjs.extend(isSameOrAfter);
dayjs.extend(quarterOfYear);
dayjs.extend(advancedFormat);
const CALENDAR_ROOT_LABEL = "calendarRoot"; const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote"; const YEAR_LABEL = "yearNote";
const QUARTER_LABEL = "quarterNote";
const MONTH_LABEL = "monthNote"; const MONTH_LABEL = "monthNote";
const WEEK_LABEL = "weekNote";
const DATE_LABEL = "dateNote"; 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 = [ const MONTH_TRANSLATION_IDS = [
"months.january", "months.january",
@ -33,14 +49,114 @@ const MONTH_TRANSLATION_IDS = [
"months.december" "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<string, string> = {
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<string, string> = {
// 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<string, string>);
return Object.entries(allowedReplacements).reduce(
(title, [ key, value ]) => title.replace(new RegExp(key, "g"), value),
pattern
);
}
function createNote(parentNote: BNote, noteTitle: string) { function createNote(parentNote: BNote, noteTitle: string) {
return noteService.createNewNote({ return noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title: noteTitle, title: noteTitle,
content: "", content: "",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: parentNote.isProtected &&
protectedSessionService.isProtectedSessionAvailable(),
type: "text" type: "text"
}).note; }).note;
} }
@ -51,7 +167,9 @@ function getRootCalendarNote(): BNote {
const workspaceNote = hoistedNoteService.getWorkspaceNote(); const workspaceNote = hoistedNoteService.getWorkspaceNote();
if (!workspaceNote || !workspaceNote.isRoot()) { if (!workspaceNote || !workspaceNote.isRoot()) {
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false })); rootNote = searchService.findFirstNoteWithQuery(
"#workspaceCalendarRoot", new searchContext({ ignoreHoistedNote: false })
);
} }
if (!rootNote) { if (!rootNote) {
@ -80,9 +198,11 @@ function getRootCalendarNote(): BNote {
function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote { function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote(); 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) { if (yearNote) {
return yearNote; return yearNote;
@ -104,38 +224,79 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
return yearNote as unknown as BNote; return yearNote as unknown as BNote;
} }
function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date) { function getQuarterNumberStr(date: Dayjs) {
const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}"; return `${date.year()}-Q${date.quarter()}`;
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 getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote { async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): Promise<BNote> {
const rootNote = _rootNote || getRootCalendarNote(); const rootNote = _rootNote || getRootCalendarNote();
const monthStr = dateStr.substr(0, 7); quarterStr = quarterStr.trim().substring(0, 7);
const monthNumber = dateStr.substr(5, 2);
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<BNote> {
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) { if (monthNote) {
return 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(() => { sql.transactional(() => {
monthNote = createNote(yearNote, noteTitle); monthNote = createNote(monthParentNote, noteTitle);
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr); attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
attributeService.createLabel(monthNote.noteId, "sorted"); attributeService.createLabel(monthNote.noteId, "sorted");
@ -150,49 +311,178 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
return monthNote as unknown as BNote; return monthNote as unknown as BNote;
} }
function getDayNoteTitle(rootNote: BNote, dayNumber: string, dateObj: Date) { function getWeekStartDate(date: Dayjs): Dayjs {
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"; const day = date.day();
const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.getDay()]); let diff;
return pattern if (optionService.getOption("firstDayOfWeek") === "0") { // Sunday
.replace(/{ordinal}/g, ordinal(parseInt(dayNumber))) diff = date.date() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
.replace(/{dayInMonthPadded}/g, dayNumber) } else { // Monday
.replace(/{isoDate}/g, dateUtils.utcDateStr(dateObj)) diff = date.date() - day;
.replace(/{weekDay}/g, weekDay) }
.replace(/{weekDay3}/g, weekDay.substr(0, 3))
.replace(/{weekDay2}/g, weekDay.substr(0, 2)); const startDate = date.clone().date(diff);
return startDate;
} }
/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */ // TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts
function ordinal(dayNumber: number) { // Maybe can be merged later in monorepo setup
const suffixes = ["th", "st", "nd", "rd"]; function getWeekNumberStr(date: Dayjs): string {
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0]; 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<BNote | null> {
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<BNote> {
const rootNote = _rootNote || getRootCalendarNote(); 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) { if (dateNote) {
return dateNote; return dateNote;
} }
const monthNote = getMonthNote(dateStr, rootNote); let dateParentNote;
const dayNumber = dateStr.substr(8, 2);
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(() => { 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"); 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) { function getTodayNote(rootNote: BNote | null = null) {
return getDayNote(dateUtils.localNowDate(), rootNote); return getDayNote(dayjs().format("YYYY-MM-DD"), 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);
} }
export default { export default {
getRootCalendarNote, getRootCalendarNote,
getYearNote, getYearNote,
getQuarterNote,
getMonthNote, getMonthNote,
getWeekNote, getWeekNote,
getWeekFirstDayNote,
getDayNote, getDayNote,
getTodayNote getTodayNote,
getJournalNoteTitle
}; };

View File

@ -143,6 +143,8 @@ const defaultOptions: DefaultOption[] = [
{ name: "locale", value: "en", isSynced: true }, { name: "locale", value: "en", isSynced: true },
{ name: "formattingLocale", value: "en", isSynced: true }, { name: "formattingLocale", value: "en", isSynced: true },
{ name: "firstDayOfWeek", value: "1", 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 }, { name: "languages", value: "[]", isSynced: true },
// Code block configuration // Code block configuration

View File

@ -85,6 +85,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
eraseUnusedAttachmentsAfterSeconds: number; eraseUnusedAttachmentsAfterSeconds: number;
eraseUnusedAttachmentsAfterTimeScale: number; eraseUnusedAttachmentsAfterTimeScale: number;
firstDayOfWeek: number; firstDayOfWeek: number;
firstWeekOfYear: number;
minDaysInFirstWeek: number;
languages: string; languages: string;
// Appearance // Appearance

View File

@ -51,12 +51,12 @@ function createSqlConsole() {
return note; return note;
} }
function saveSqlConsole(sqlConsoleNoteId: string) { async function saveSqlConsole(sqlConsoleNoteId: string) {
const sqlConsoleNote = becca.getNote(sqlConsoleNoteId); const sqlConsoleNote = becca.getNote(sqlConsoleNoteId);
if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`); if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`);
const today = dateUtils.localNowDate(); const today = dateUtils.localNowDate();
const sqlConsoleHome = attributeService.getNoteWithLabel("sqlConsoleHome") || dateNoteService.getDayNote(today); const sqlConsoleHome = attributeService.getNoteWithLabel("sqlConsoleHome") || await dateNoteService.getDayNote(today);
const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId); const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);

View File

@ -1,23 +1,24 @@
import { Menu, Tray, BrowserWindow } from "electron"; import { BrowserWindow,Menu, Tray } from "electron";
import path from "path";
import windowService from "./window.js";
import optionService from "./options.js";
import { fileURLToPath } from "url";
import type { KeyboardActionNames } from "./keyboard_actions_interface.js";
import date_notes from "./date_notes.js";
import type BNote from "../becca/entities/bnote.js";
import becca from "../becca/becca.js";
import becca_service from "../becca/becca_service.js";
import type BRecentNote from "../becca/entities/brecent_note.js";
import { ipcMain, nativeTheme } from "electron/main"; import { ipcMain, nativeTheme } from "electron/main";
import { default as i18next, t } from "i18next"; import { default as i18next, t } from "i18next";
import { isDev, isMac } from "./utils.js"; import path from "path";
import { fileURLToPath } from "url";
import becca from "../becca/becca.js";
import becca_service from "../becca/becca_service.js";
import type BNote from "../becca/entities/bnote.js";
import type BRecentNote from "../becca/entities/brecent_note.js";
import cls from "./cls.js"; import cls from "./cls.js";
import date_notes from "./date_notes.js";
import type { KeyboardActionNames } from "./keyboard_actions_interface.js";
import optionService from "./options.js";
import { isDev, isMac } from "./utils.js";
import windowService from "./window.js";
let tray: Tray; let tray: Tray;
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
// is minimized // is minimized
let windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status const windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status
function getTrayIconPath() { function getTrayIconPath() {
let name: string; let name: string;
@ -75,7 +76,7 @@ function updateWindowVisibilityMap(allWindows: BrowserWindow[]) {
const currentWindowIds: number[] = allWindows.map(window => window.id); const currentWindowIds: number[] = allWindows.map(window => window.id);
// Deleting closed windows from windowVisibilityMap // Deleting closed windows from windowVisibilityMap
for (const [id, visibility] of Object.entries(windowVisibilityMap)) { for (const [id, _] of Object.entries(windowVisibilityMap)) {
const windowId = Number(id); const windowId = Number(id);
if (!currentWindowIds.includes(windowId)) { if (!currentWindowIds.includes(windowId)) {
delete windowVisibilityMap[windowId]; delete windowVisibilityMap[windowId];
@ -133,7 +134,7 @@ function updateTrayMenu() {
const parentNote = becca.getNoteOrThrow("_lbBookmarks"); const parentNote = becca.getNoteOrThrow("_lbBookmarks");
const menuItems: Electron.MenuItemConstructorOptions[] = []; const menuItems: Electron.MenuItemConstructorOptions[] = [];
for (const bookmarkNote of parentNote?.children) { for (const bookmarkNote of parentNote?.children ?? []) {
if (bookmarkNote.isLabelTruthy("bookmarkFolder")) { if (bookmarkNote.isLabelTruthy("bookmarkFolder")) {
menuItems.push({ menuItems.push({
label: bookmarkNote.title, label: bookmarkNote.title,
@ -194,7 +195,7 @@ function updateTrayMenu() {
for (const idStr in windowVisibilityMap) { for (const idStr in windowVisibilityMap) {
const id = parseInt(idStr, 10); // Get the ID of the window and make sure it is a number 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); const win = allWindows.find(w => w.id === id);
if (!win) { if (!win) {
continue; continue;
@ -214,10 +215,10 @@ function updateTrayMenu() {
} }
}); });
} }
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
...windowVisibilityMenuItems, ...windowVisibilityMenuItems,
{ type: "separator" }, { type: "separator" },
{ {
label: t("tray.open_new_window"), label: t("tray.open_new_window"),
@ -235,7 +236,7 @@ function updateTrayMenu() {
label: t("tray.today"), label: t("tray.today"),
type: "normal", type: "normal",
icon: getIconPath("today"), icon: getIconPath("today"),
click: cls.wrap(() => openInSameTab(date_notes.getTodayNote())) click: cls.wrap(async () => openInSameTab(await date_notes.getTodayNote()))
}, },
{ {
label: t("tray.bookmarks"), label: t("tray.bookmarks"),
@ -268,7 +269,7 @@ function updateTrayMenu() {
function changeVisibility() { function changeVisibility() {
const lastFocusedWindow = windowService.getLastFocusedWindow(); const lastFocusedWindow = windowService.getLastFocusedWindow();
if (!lastFocusedWindow) { if (!lastFocusedWindow) {
return; return;
} }

View File

@ -174,6 +174,7 @@
"saturday": "周六", "saturday": "周六",
"sunday": "周日" "sunday": "周日"
}, },
"weekdayNumber": "第 {weekNumber} 周",
"months": { "months": {
"january": "一月", "january": "一月",
"february": "二月", "february": "二月",
@ -188,6 +189,7 @@
"november": "十一月", "november": "十一月",
"december": "十二月" "december": "十二月"
}, },
"quarterNumber": "第 {quarterNumber} 季度",
"special_notes": { "special_notes": {
"search_prefix": "搜索:" "search_prefix": "搜索:"
}, },

View File

@ -174,6 +174,7 @@
"saturday": "Saturday", "saturday": "Saturday",
"sunday": "Sunday" "sunday": "Sunday"
}, },
"weekdayNumber": "Week {weekNumber}",
"months": { "months": {
"january": "January", "january": "January",
"february": "February", "february": "February",
@ -188,6 +189,7 @@
"november": "November", "november": "November",
"december": "December" "december": "December"
}, },
"quarterNumber": "Quarter {quarterNumber}",
"special_notes": { "special_notes": {
"search_prefix": "Search:" "search_prefix": "Search:"
}, },