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',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'weekPattern', 'enableWeekNote', 'monthPattern',
'quarterPattern', 'yearPattern', 'enableQuarterNote', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
@ -31,7 +32,8 @@ UPDATE attributes SET name = 'name' WHERE type = 'relation'
'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'weekPattern', 'enableWeekNote', 'monthPattern',
'quarterPattern', 'yearPattern', 'enableQuarterNote', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',

View File

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

View File

@ -13,10 +13,14 @@ This pattern works well also because of [Cloning Notes](../../Basic%20Concepts%2
![](Day%20Notes_image.png)
You can see the structure of day notes appearing under "Journal" note - there's a note for the whole year 2017, under it, you have "12 - December" which then contains "18 - Monday". This is our "day note" which contains some text in its content and also has some child notes (some of them are from [Task manager](Task%20Manager.md)).
You can see the structure of day notes appearing under "Journal" note - there's a note for the whole year 2025, under it, you have "03 - March" which then contains "09 - Monday". This is our "day note" which contains some text in its content and also has some child notes (some of them are from [Task manager](Task%20Manager.md)).
You can also notice how this day note has [promoted attribute](../Attributes/Promoted%20Attributes.md) "weight" where you can track your daily weight. This data is then used in [Weight tracker](Weight%20Tracker.md).
## Week Note and Quarter Note
Week and quarter notes are disabled by default, since it might be too much for some people. To enable them, you need to set `#enableWeekNotes` and `#enableQuarterNotes` attributes on the root calendar note, which is identified by `#calendarRoot` label. Week note is affected by the first week of year option. Be careful when you already have some week notes created, it will not automatically change the existing week notes and might lead to some duplicates.
## Templates
Trilium provides [template](../Templates.md) functionality, and it could be used together with day notes.
@ -24,26 +28,48 @@ Trilium provides [template](../Templates.md) functionality, and it could be used
You can define one of the following relations on the root of the journal (identified by `#calendarRoot` label):
* yearTemplate
* quarterTemplate (if `#enableQuarterNotes` is set)
* monthTemplate
* weekTemplate (if `#enableWeekNotes` is set)
* dateTemplate
All of these are relations. When Trilium creates a new note for year or month or date, it will take a look at the root and attach a corresponding `~template` relation to the newly created role. Using this, you can e.g. create your daily template with e.g. checkboxes for daily routine etc.
## Date pattern
## Naming pattern
It's possible to customize the title of generated date notes by defining a `#datePattern` label on a root calendar note (identified by `#calendarRoot` label). Following are possible values:
You can customize the title of generated journal notes by defining a `#datePattern`, `#weekPattern`, `#monthPattern`, `#quarterPattern` and `#yearPattern` attribute on a root calendar note (identified by `#calendarRoot` label). The naming pattern replacements follow a level-up compatibility - each level can use replacements from itself and all levels above it. For example, `#monthPattern` can use month, quarter and year replacements, while `#weekPattern` can use week, month, quarter and year replacements. But it is not possible to use week replacements in `#monthPattern`.
* `{dayInMonthPadded} - {weekDay}` day notes are named e.g. "24 - Monday"
* `{dayInMonthPadded}: {weekDay3}` day notes are named e.g. "24: Mon"
* `{dayInMonthPadded}: {weekDay2}` day notes are named e.g. "24: Mo"
* `{isoDate} - {weekDay}` day notes are named e.g. "2020-12-24 - Monday"
### Date pattern
It's possible to customize the title of generated date notes by defining a `#datePattern` attribute on a root calendar note (identified by `#calendarRoot` label). Following are possible values:
* `{isoDate}` results in an ISO 8061 formatted date (e.g. "2025-03-09" for March 9, 2025)
* `{dateNumber}` results in a number like `9` for the 9th day of the month, `11` for the 11th day of the month
* `{dateNumberPadded}` results in a number like `09` for the 9th day of the month, `11` for the 11th day of the month
* `{ordinal}` is replaced with the ordinal date (e.g. 1st, 2nd, 3rd) etc.
* `{weekDay}` results in the full day name (e.g. `Monday`)
* `{weekDay3}` is replaced with the first 3 letters of the day, e.g. Mon, Tue, etc.
* `{weekDay2}` is replaced with the first 2 letters of the day, e.g. Mo, Tu, etc.
## Month pattern
The default is `{dateNumberPadded} - {weekDay}`
It is also possible to customize the title of generated month notes through the `#monthPattern` attribute, much like `#datePattern`. The options are:
### Week pattern
It is also possible to customize the title of generated week notes through the `#weekPattern` attribute on the root calendar note. The options are:
* `{weekNumber}` results in a number like `9` for the 9th week of the year, `11` for the 11th week of the year
* `{weekNumberPadded}` results in a number like `09` for the 9th week of the year, `11` for the 11th week of the year
* `{shortWeek}` results in a short week string like `W9` for the 9th week of the year, `W11` for the 11th week of the year
* `{shortWeek3}` results in a short week string like `W09` for the 9th week of the year, `W11` for the 11th week of the year
The default is `Week {weekNumber}`
### Month pattern
It is also possible to customize the title of generated month notes through the `#monthPattern` attribute on the root calendar note. The options are:
* `{isoMonth}` results in an ISO 8061 formatted month (e.g. "2025-03" for March 2025)
* `{monthNumber}` results in a number like `9` for September, and `11` for November
* `{monthNumberPadded}` results in a number like `09` for September, and `11` for November
* `{month}` results in the full month name (e.g. `September` or `October`)
* `{shortMonth3}` is replaced with the first 3 letters of the month, e.g. Jan, Feb, etc.
@ -51,10 +77,27 @@ It is also possible to customize the title of generated month notes through the
The default is `{monthNumberPadded} - {month}`
### Quarter pattern
It is also possible to customize the title of generated quarter notes through the `#quarterPattern` attribute on the root calendar note. The options are:
* `{quarterNumber}` results in a number like `1` for the 1st quarter of the year
* `{shortQuarter}` results in a short quarter string like `Q1` for the 1st quarter of the year
The default is `Quarter {quarterNumber}`
### Year pattern
It is also possible to customize the title of generated year notes through the `#yearPattern` attribute on the root calendar note. The options are:
* `{year}` results in the full year (e.g. `2025`)
The default is `{year}`
## Implementation
Trilium has some special support for day notes in the form of [backend Script API](https://triliumnext.github.io/Notes/backend_api/BackendScriptApi.html) - see e.g. getDayNote() function.
Day (and year, month) notes are created with a label - e.g. `#dateNote="2018-08-16"` this can then be used by other scripts to add new notes to day note etc.
Day (and year, month) notes are created with a label - e.g. `#dateNote="2025-03-09"` this can then be used by other scripts to add new notes to day note etc.
Journal also has relation `child:child:child:template=Day template` (see \[\[attribute inheritance\]\]) which effectively adds \[\[template\]\] to day notes (grand-grand-grand children of Journal).
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"
* @returns {Promise&lt;FNote>}
*/
this.getWeekNote = dateNotesService.getWeekNote;
this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote;
/**
* Returns month-note. If it doesn't exist, it is automatically created.

View File

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

View File

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

View File

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

View File

@ -35,12 +35,19 @@
<img src="Day Notes_image.png">
</p>
<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"
which then contains "18 - Monday". This is our "day note" which contains
- there's a note for the whole year 2025, under it, you have "03 - March"
which then contains "09 - Monday". This is our "day note" which contains
some text in its content and also has some child notes (some of them are
from <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"
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>
<p>Trilium provides <a href="#root/_help_KC1HB96bqqHX">template</a> functionality,
and it could be used together with day notes.</p>
@ -48,36 +55,69 @@
(identified by <code>#calendarRoot</code> label):</p>
<ul>
<li>yearTemplate</li>
<li>quarterTemplate (if <code>#enableQuarterNotes</code> is set)</li>
<li>monthTemplate</li>
<li>weekTemplate (if <code>#enableWeekNotes</code> is set)</li>
<li>dateTemplate</li>
</ul>
<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
to the newly created role. Using this, you can e.g. create your daily template
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
a <code>#datePattern</code> label on a root calendar note (identified by <code>#calendarRoot</code> label).
Following are possible values:</p>
a <code>#datePattern</code> attribute on a root calendar note (identified
by <code>#calendarRoot</code> label). Following are possible values:</p>
<ul>
<li><code>{dayInMonthPadded} - {weekDay}</code> day notes are named e.g. "24
- Monday"</li>
<li><code>{dayInMonthPadded}: {weekDay3}</code> day notes are named e.g. "24:
Mon"</li>
<li><code>{dayInMonthPadded}: {weekDay2}</code> day notes are named e.g. "24:
Mo"</li>
<li><code>{isoDate} - {weekDay}</code> day notes are named e.g. "2020-12-24
- Monday"</li>
<li><code>{isoDate}</code> results in an ISO 8061 formatted date (e.g. "2025-03-09"
for March 9, 2025)</li>
<li><code>{dateNumber}</code> results in a number like <code>9</code> for the
9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{dateNumberPadded}</code> results in a number like <code>09</code> for
the 9th day of the month, <code>11</code> for the 11th day of the month</li>
<li><code>{ordinal}</code> is replaced with the ordinal date (e.g. 1st, 2nd,
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>
<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
the <code>#monthPattern</code> attribute, much like <code>#datePattern</code>.
The options are:</p>
the <code>#monthPattern</code> attribute on the root calendar note. The options
are:</p>
<ul>
<li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g. "2025-03"
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
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>
@ -88,14 +128,37 @@
</ul>
<p>The default is <code>{monthNumberPadded} - {month}</code>
</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>
<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>
<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>
<p>Journal also has relation <code>child:child:child:template=Day template</code> (see
[[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>
</body>

View File

@ -21,19 +21,18 @@
<h2>Alternatives</h2>
<ul>
<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
a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note
will trigger the browser's native search. This will also find text that
is part of Trilium's UI.</li>
<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/pOsGYCXsbNQG/tC7s2alapj8V/1YeN2MzFUluU/_help_MI26XDLSAlCD">CKEditor</a>'s
href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;or a&nbsp;<a class="reference-link"
href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note will trigger the browser's
native search. This will also find text that 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
will reveal&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>'s
search functionality.</li>
</ul>
<h2>Accessing the search</h2>
<ul>
<li>On desktop, press<kbd>Ctrl</kbd> + <kbd>F</kbd>
</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>
</ul>
<h2>Interaction</h2>

View File

@ -24,10 +24,10 @@
results as sub-items.</p>
<h2>Accessing the search</h2>
<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>
<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>
</ul>
<h2>Interaction</h2>
@ -43,8 +43,8 @@
</li>
<li>To limit the search to a note and its sub-children, set a note in <em>Ancestor</em>.
<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
a <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_9sRHySam5fXb">workspace</a>.</li>
<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/_help_9sRHySam5fXb">workspace</a>.</li>
<li>To search the entire database, keep the value empty.</li>
</ol>
</li>
@ -58,7 +58,7 @@
<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>
<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>
<h2>Search options</h2>
<p>Click on which search option to apply from the Add search option section.</p>
@ -71,7 +71,7 @@
<ol>
<li>Search script
<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>
</ol>
</li>
@ -79,12 +79,12 @@
<ol>
<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>
<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>
</li>
<li>Include archived
<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>
</ol>
</li>
@ -107,7 +107,7 @@
<ol>
<li>This will print additional information in the server log (see&nbsp;
<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
in detail, in order to determine why a complex search query is not working
as expected.</li>
@ -122,10 +122,9 @@
action multiple times (i.e. in order to be able to apply multiple labels
to notes).</li>
<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>,
which is an alternative for operating directly with notes within the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
href="#root/_help_ivYnonVFBxbQ">Bulk Actions</a>, which is an alternative
for operating directly with notes within the&nbsp;<a class="reference-link"
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
<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
actions.</li>

View File

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

View File

@ -363,6 +363,14 @@ interface Api {
*
* @param date - e.g. "2019-04-29"
*/
getWeekFirstDayNote: typeof dateNotesService.getWeekFirstDayNote;
/**
* Returns week note for given date. If such a note doesn't exist, it is automatically created.
*
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getWeekNote: typeof dateNotesService.getWeekNote;
/**
@ -372,6 +380,14 @@ interface Api {
*/
getMonthNote: typeof dateNotesService.getMonthNote;
/**
* Returns quarter note for given date. If such a note doesn't exist, it is automatically created.
*
* @param date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getQuarterNote: typeof dateNotesService.getQuarterNote;
/**
* Returns year-note. If it doesn't exist, it is automatically created.
*
@ -651,8 +667,10 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.getTodayNote = dateNotesService.getTodayNote;
this.getDayNote = dateNotesService.getDayNote;
this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote;
this.getWeekNote = dateNotesService.getWeekNote;
this.getMonthNote = dateNotesService.getMonthNote;
this.getQuarterNote = dateNotesService.getQuarterNote;
this.getYearNote = dateNotesService.getYearNote;
this.setHoistedNoteId = (noteId) => {

View File

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

View File

@ -35,6 +35,41 @@ const TPL = /*html*/`
</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">
<button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
</div>
@ -64,6 +99,16 @@ const TPL = /*html*/`
.locale-options-container .option-row.centered {
justify-content: center;
}
.locale-options-container .option-row [aria-labelledby="first-week-of-year-label"] {
display: flex;
flex-direction: column;
}
.locale-options-container .option-row [aria-labelledby="first-week-of-year-label"] .tn-radio {
margin-left: 0;
white-space: nowrap;
}
</style>
</div>
`;
@ -72,10 +117,13 @@ export default class LocalizationOptions extends OptionsWidget {
private $localeSelect!: JQuery<HTMLElement>;
private $formattingLocaleSelect!: JQuery<HTMLElement>;
private $minDaysRow!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$minDaysRow = this.$widget.find(".min-days-row");
this.$localeSelect = this.$widget.find(".locale-select");
this.$localeSelect.on("change", async () => {
const newLocale = this.$localeSelect.val();
@ -92,6 +140,30 @@ export default class LocalizationOptions extends OptionsWidget {
const firstDayOfWeek = String(this.$widget.find(`input[name="first-day-of-week"]:checked`).val());
this.updateOption("firstDayOfWeek", firstDayOfWeek);
});
this.$widget.find('input[name="first-week-of-year"]').on('change', (e) => {
const target = e.target as HTMLInputElement;
const value = parseInt(target.value);
if (value === 2) {
this.$minDaysRow.show();
} else {
this.$minDaysRow.hide();
}
this.updateOption("firstWeekOfYear", value);
});
const currentValue = this.$widget.find('input[name="first-week-of-year"]:checked').val();
if (currentValue === 2) {
this.$minDaysRow.show();
}
this.$widget.find("#min-days-in-first-week").on("change", () => {
const minDays = this.$widget.find("#min-days-in-first-week").val();
this.updateOption("minDaysInFirstWeek", minDays);
});
this.$widget.find(".restart-app-button").on("click", utils.restartDesktopApp);
}
@ -119,6 +191,15 @@ export default class LocalizationOptions extends OptionsWidget {
this.$formattingLocaleSelect.val(options.formattingLocale);
this.$widget.find(`input[name="first-day-of-week"][value="${options.firstDayOfWeek}"]`)
.prop("checked", "true");
.prop("checked", "true");
this.$widget.find(`input[name="first-week-of-year"][value="${options.firstWeekOfYear}"]`)
.prop("checked", "true");
if (parseInt(options.firstWeekOfYear) === 2) {
this.$minDaysRow.show();
}
this.$widget.find("#min-days-in-first-week").val(options.minDaysInFirstWeek);
}
}

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

View File

@ -35,7 +35,7 @@
padding: 0 0.5rem 0.5rem 0.5rem;
}
.calendar-dropdown-widget .calendar-header > div {
.calendar-dropdown-widget .calendar-header>div {
display: flex;
justify-content: center;
flex-grow: 1;
@ -67,7 +67,8 @@
}
.calendar-dropdown-widget .calendar-header .dropdown-toggle::after {
border: unset; /* Disable the dropdown arrow */
border: unset;
/* Disable the dropdown arrow */
}
.calendar-dropdown-widget .calendar-week {
@ -77,10 +78,10 @@
.calendar-dropdown-widget .calendar-week span {
flex-direction: column;
flex: 0 0 14.28%;
flex: 0 0 12.5%;
font-size: 1rem;
font-weight: bold;
max-width: 14.28%;
max-width: 12.5%;
padding-top: 5px;
padding-bottom: 5px;
text-align: center;
@ -92,13 +93,40 @@
flex-wrap: wrap;
}
.calendar-dropdown-widget .calendar-week-number {
color: var(--muted-text-color) !important;
position: relative;
}
.calendar-dropdown-widget .calendar-week-number::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1px;
background-color: var(--main-border-color);
z-index: 2;
}
.calendar-dropdown-widget .calendar-week-number-disabled {
align-items: center;
color: var(--main-text-color);
display: flex;
flex-direction: column;
flex: 0 0 12.5%;
max-width: 12.5%;
padding: 0.4rem 0;
font-size: 120%;
}
.calendar-dropdown-widget .calendar-date {
align-items: center;
color: var(--main-text-color);
display: flex;
flex-direction: column;
flex: 0 0 14.28%;
max-width: 14.28%;
flex: 0 0 12.5%;
max-width: 12.5%;
padding: 0.4rem 0;
font-size: 120%;
}
@ -129,3 +157,17 @@
.calendar-dropdown-widget .calendar-date:not(.calendar-date-active) {
cursor: pointer;
}
.calendar-dropdown-widget .calendar-date-prev-month,
.calendar-dropdown-widget .calendar-date-next-month {
color: var(--muted-text-color);
opacity: 0.6;
}
.calendar-dropdown-widget .calendar-date-prev-month:hover,
.calendar-dropdown-widget .calendar-date-next-month:hover {
opacity: 1;
background-color: var(--hover-item-background-color);
color: var(--hover-item-text-color);
text-decoration: underline;
}

View File

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

View File

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

View File

@ -588,6 +588,7 @@
"sat": "Sat",
"sun": "Sun",
"cannot_find_day_note": "Cannot find day note",
"cannot_find_week_note": "Cannot find week note",
"january": "January",
"febuary": "February",
"march": "March",
@ -1242,6 +1243,13 @@
"first-day-of-the-week": "First day of the week",
"sunday": "Sunday",
"monday": "Monday",
"first-week-of-the-year": "First week of the year",
"first-week-contains-first-day": "First week contains first day of the year",
"first-week-contains-first-thursday": "First week contains first Thursday of the year",
"first-week-has-minimum-days": "First week has minimum days",
"min-days-in-first-week": "Minimum days in first week",
"first-week-info": "First week contains first Thursday of the year is based on <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"
},
"backup": {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
"use strict";
import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js";
import multer from "multer";
import log from "../services/log.js";
@ -308,8 +306,10 @@ function register(app: express.Application) {
apiRoute(GET, "/api/special-notes/inbox/:date", specialNotesRoute.getInboxNote);
apiRoute(GET, "/api/special-notes/days/:date", specialNotesRoute.getDayNote);
apiRoute(GET, "/api/special-notes/weeks/:date", specialNotesRoute.getWeekNote);
apiRoute(GET, "/api/special-notes/week-first-day/:date", specialNotesRoute.getWeekFirstDayNote);
apiRoute(GET, "/api/special-notes/weeks/:week", specialNotesRoute.getWeekNote);
apiRoute(GET, "/api/special-notes/months/:month", specialNotesRoute.getMonthNote);
apiRoute(GET, "/api/special-notes/quarters/:quarter", specialNotesRoute.getQuarterNote);
apiRoute(GET, "/api/special-notes/years/:year", specialNotesRoute.getYearNote);
apiRoute(GET, "/api/special-notes/notes-for-month/:month", specialNotesRoute.getDayNotesForMonth);
apiRoute(PST, "/api/special-notes/sql-console", specialNotesRoute.createSqlConsole);

View File

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

View File

@ -36,6 +36,12 @@ export default [
{ type: "label", name: "workspaceSearchHome" },
{ type: "label", name: "sqlConsoleHome" },
{ type: "label", name: "datePattern" },
{ type: "label", name: "weekPattern" },
{ type: "label", name: "enableWeekNote" },
{ type: "label", name: "monthPattern" },
{ type: "label", name: "quarterPattern" },
{ type: "label", name: "yearPattern" },
{ type: "label", name: "enableQuarterNote" },
{ type: "label", name: "pageSize" },
{ type: "label", name: "viewType" },
{ type: "label", name: "mapRootNoteId" },

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 { Dayjs } from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat.js";
import attributeService from "./attributes.js";
import cloningService from "./cloning.js";
import dayjs from "dayjs";
import hoistedNoteService from "./hoisted_note.js";
import i18next from "i18next";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
import noteService from "./notes.js";
import optionService from "./options.js";
import protectedSessionService from "./protected_session.js";
import quarterOfYear from "dayjs/plugin/quarterOfYear.js";
import searchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import sql from "./sql.js";
import { t } from "i18next";
dayjs.extend(isSameOrAfter);
dayjs.extend(quarterOfYear);
dayjs.extend(advancedFormat);
const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote";
const QUARTER_LABEL = "quarterNote";
const MONTH_LABEL = "monthNote";
const WEEK_LABEL = "weekNote";
const DATE_LABEL = "dateNote";
const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"];
const WEEKDAY_TRANSLATION_IDS = [
"weekdays.sunday", "weekdays.monday", "weekdays.tuesday",
"weekdays.wednesday", "weekdays.thursday", "weekdays.friday",
"weekdays.saturday", "weekdays.sunday"
];
const MONTH_TRANSLATION_IDS = [
"months.january",
@ -33,14 +49,114 @@ const MONTH_TRANSLATION_IDS = [
"months.december"
];
type StartOfWeek = "monday" | "sunday";
type TimeUnit = "year" | "quarter" | "month" | "week" | "day";
const baseReplacements = {
year: [ "year" ],
quarter: [ "quarterNumber", "shortQuarter" ],
month: [ "isoMonth", "monthNumber", "monthNumberPadded",
"month", "shortMonth3", "shortMonth4" ],
week: [ "weekNumber", "weekNumberPadded", "shortWeek", "shortWeek3" ],
day: [ "isoDate", "dateNumber", "dateNumberPadded",
"ordinal", "weekDay", "weekDay3", "weekDay2" ]
};
function getTimeUnitReplacements(timeUnit: TimeUnit): string[] {
const units: TimeUnit[] = [ "year", "quarter", "month", "week", "day" ];
const index = units.indexOf(timeUnit);
return units.slice(0, index + 1).flatMap(unit => baseReplacements[unit]);
}
async function ordinal(date: Dayjs, lng: string) {
const localeMap: Record<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) {
return noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: noteTitle,
content: "",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
isProtected: parentNote.isProtected &&
protectedSessionService.isProtectedSessionAvailable(),
type: "text"
}).note;
}
@ -51,7 +167,9 @@ function getRootCalendarNote(): BNote {
const workspaceNote = hoistedNoteService.getWorkspaceNote();
if (!workspaceNote || !workspaceNote.isRoot()) {
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false }));
rootNote = searchService.findFirstNoteWithQuery(
"#workspaceCalendarRoot", new searchContext({ ignoreHoistedNote: false })
);
}
if (!rootNote) {
@ -80,9 +198,11 @@ function getRootCalendarNote(): BNote {
function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const rootNote = _rootNote || getRootCalendarNote();
const yearStr = dateStr.trim().substr(0, 4);
const yearStr = dateStr.trim().substring(0, 4);
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
let yearNote = searchService.findFirstNoteWithQuery(
`#${YEAR_LABEL}="${yearStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId })
);
if (yearNote) {
return yearNote;
@ -104,38 +224,79 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
return yearNote as unknown as BNote;
}
function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date) {
const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.getMonth()]);
return pattern
.replace(/{shortMonth3}/g, monthName.slice(0, 3))
.replace(/{shortMonth4}/g, monthName.slice(0, 4))
.replace(/{isoMonth}/g, dateUtils.utcDateStr(dateObj).slice(0, 7))
.replace(/{monthNumberPadded}/g, monthNumber)
.replace(/{month}/g, monthName);
function getQuarterNumberStr(date: Dayjs) {
return `${date.year()}-Q${date.quarter()}`;
}
function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
async function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): Promise<BNote> {
const rootNote = _rootNote || getRootCalendarNote();
const monthStr = dateStr.substr(0, 7);
const monthNumber = dateStr.substr(5, 2);
quarterStr = quarterStr.trim().substring(0, 7);
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
let quarterNote = searchService.findFirstNoteWithQuery(
`#${QUARTER_LABEL}="${quarterStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId })
);
if (quarterNote) {
return quarterNote;
}
const [ yearStr, quarterNumberStr ] = quarterStr.trim().split("-Q");
const quarterNumber = parseInt(quarterNumberStr);
const firstMonth = (quarterNumber - 1) * 3;
const quarterStartDate = dayjs().year(parseInt(yearStr)).month(firstMonth).date(1);
const yearNote = getYearNote(yearStr, rootNote);
const noteTitle = await getJournalNoteTitle(
rootNote, "quarter", quarterStartDate, quarterNumber
);
sql.transactional(() => {
quarterNote = createNote(yearNote, noteTitle);
attributeService.createLabel(quarterNote.noteId, QUARTER_LABEL, quarterStr);
attributeService.createLabel(quarterNote.noteId, "sorted");
const quarterTemplateAttr = rootNote.getOwnedAttribute("relation", "quarterTemplate");
if (quarterTemplateAttr) {
attributeService.createRelation(
quarterNote.noteId, "template", quarterTemplateAttr.value
);
}
});
return quarterNote as unknown as BNote;
}
async function getMonthNote(dateStr: string, _rootNote: BNote | null = null): Promise<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) {
return monthNote;
}
const dateObj = dateUtils.parseLocalDate(dateStr);
let monthParentNote;
const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj);
if (rootNote.hasLabel("enableQuarterNote")) {
monthParentNote = await getQuarterNote(getQuarterNumberStr(dayjs(dateStr)), rootNote);
} else {
monthParentNote = getYearNote(dateStr, rootNote);
}
const yearNote = getYearNote(dateStr, rootNote);
const noteTitle = await getJournalNoteTitle(
rootNote, "month", dayjs(dateStr), parseInt(monthNumber)
);
sql.transactional(() => {
monthNote = createNote(yearNote, noteTitle);
monthNote = createNote(monthParentNote, noteTitle);
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
attributeService.createLabel(monthNote.noteId, "sorted");
@ -150,49 +311,178 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
return monthNote as unknown as BNote;
}
function getDayNoteTitle(rootNote: BNote, dayNumber: string, dateObj: Date) {
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.getDay()]);
function getWeekStartDate(date: Dayjs): Dayjs {
const day = date.day();
let diff;
return pattern
.replace(/{ordinal}/g, ordinal(parseInt(dayNumber)))
.replace(/{dayInMonthPadded}/g, dayNumber)
.replace(/{isoDate}/g, dateUtils.utcDateStr(dateObj))
.replace(/{weekDay}/g, weekDay)
.replace(/{weekDay3}/g, weekDay.substr(0, 3))
.replace(/{weekDay2}/g, weekDay.substr(0, 2));
if (optionService.getOption("firstDayOfWeek") === "0") { // Sunday
diff = date.date() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
} else { // Monday
diff = date.date() - day;
}
const startDate = date.clone().date(diff);
return startDate;
}
/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */
function ordinal(dayNumber: number) {
const suffixes = ["th", "st", "nd", "rd"];
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0];
// TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts
// Maybe can be merged later in monorepo setup
function getWeekNumberStr(date: Dayjs): string {
const year = date.year();
const dayOfWeek = (day: number) =>
(day - parseInt(optionService.getOption("firstDayOfWeek")) + 7) % 7;
return `${dayNumber}${suffix}`;
// Get first day of the year and adjust to first week start
const jan1 = date.clone().year(year).month(0).date(1);
const jan1Weekday = jan1.day();
const dayOffset = dayOfWeek(jan1Weekday);
let firstWeekStart = jan1.clone().subtract(dayOffset, "day");
// Adjust based on week rule
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: { // ISO 8601: first week contains Thursday
const thursday = firstWeekStart.clone().add(3, "day"); // Monday + 3 = Thursday
if (thursday.year() < year) {
firstWeekStart = firstWeekStart.add(7, "day");
}
break;
}
case 2: { // minDaysInFirstWeek rule
const daysInFirstWeek = 7 - dayOffset;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
firstWeekStart = firstWeekStart.add(7, "day");
}
break;
}
// default case 0: week containing Jan 1 → already handled
}
const diffDays = date.startOf("day").diff(firstWeekStart.startOf("day"), "day");
const weekNumber = Math.floor(diffDays / 7) + 1;
// Handle case when date is before first week start → belongs to last week of previous year
if (weekNumber <= 0) {
return getWeekNumberStr(date.subtract(1, "day"));
}
// Handle case when date belongs to first week of next year
const nextYear = year + 1;
const jan1Next = date.clone().year(nextYear).month(0).date(1);
const jan1WeekdayNext = jan1Next.day();
const offsetNext = dayOfWeek(jan1WeekdayNext);
let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, "day");
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: {
const thursday = nextYearWeekStart.clone().add(3, "day");
if (thursday.year() < nextYear) {
nextYearWeekStart = nextYearWeekStart.add(7, "day");
}
break;
}
case 2: {
const daysInFirstWeek = 7 - offsetNext;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
nextYearWeekStart = nextYearWeekStart.add(7, "day");
}
break;
}
}
if (date.isSameOrAfter(nextYearWeekStart)) {
return `${nextYear}-W01`;
}
return `${year}-W${weekNumber.toString().padStart(2, "0")}`;
}
function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
const weekStartDate = getWeekStartDate(dayjs(dateStr));
return getDayNote(weekStartDate.format("YYYY-MM-DD"), rootNote);
}
async function getWeekNote(weekStr: string, _rootNote: BNote | null = null): Promise<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();
dateStr = dateStr.trim().substr(0, 10);
dateStr = dateStr.trim().substring(0, 10);
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
let dateNote = searchService.findFirstNoteWithQuery(
`#${DATE_LABEL}="${dateStr}"`, new searchContext({ ancestorNoteId: rootNote.noteId })
);
if (dateNote) {
return dateNote;
}
const monthNote = getMonthNote(dateStr, rootNote);
const dayNumber = dateStr.substr(8, 2);
let dateParentNote;
const dateObj = dateUtils.parseLocalDate(dateStr);
if (rootNote.hasLabel("enableWeekNote")) {
dateParentNote = await getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote);
} else {
dateParentNote = await getMonthNote(dateStr, rootNote);
}
const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj);
const dayNumber = dateStr.substring(8, 10);
const noteTitle = await getJournalNoteTitle(
rootNote, "day", dayjs(dateStr), parseInt(dayNumber)
);
sql.transactional(() => {
dateNote = createNote(monthNote, noteTitle);
dateNote = createNote(dateParentNote as BNote, noteTitle);
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substring(0, 10));
const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate");
@ -205,43 +495,17 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
}
function getTodayNote(rootNote: BNote | null = null) {
return getDayNote(dateUtils.localNowDate(), rootNote);
}
function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
const day = date.getDay();
let diff;
if (startOfTheWeek === "monday") {
diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
} else if (startOfTheWeek === "sunday") {
diff = date.getDate() - day;
} else {
throw new Error(`Unrecognized start of the week ${startOfTheWeek}`);
}
return new Date(date.setDate(diff));
}
interface WeekNoteOpts {
startOfTheWeek?: StartOfWeek;
}
function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote: BNote | null = null) {
const startOfTheWeek = options.startOfTheWeek || "monday";
const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek);
dateStr = dateUtils.utcDateTimeStr(dateObj);
return getDayNote(dateStr, rootNote);
return getDayNote(dayjs().format("YYYY-MM-DD"), rootNote);
}
export default {
getRootCalendarNote,
getYearNote,
getQuarterNote,
getMonthNote,
getWeekNote,
getWeekFirstDayNote,
getDayNote,
getTodayNote
getTodayNote,
getJournalNoteTitle
};

View File

@ -143,6 +143,8 @@ const defaultOptions: DefaultOption[] = [
{ name: "locale", value: "en", isSynced: true },
{ name: "formattingLocale", value: "en", isSynced: true },
{ name: "firstDayOfWeek", value: "1", isSynced: true },
{ name: "firstWeekOfYear", value: "0", isSynced: true },
{ name: "minDaysInFirstWeek", value: "4", isSynced: true },
{ name: "languages", value: "[]", isSynced: true },
// Code block configuration

View File

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

View File

@ -51,12 +51,12 @@ function createSqlConsole() {
return note;
}
function saveSqlConsole(sqlConsoleNoteId: string) {
async function saveSqlConsole(sqlConsoleNoteId: string) {
const sqlConsoleNote = becca.getNote(sqlConsoleNoteId);
if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`);
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);

View File

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

View File

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

View File

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