mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-10-18 04:21:32 +08:00
Merge pull request #1579 from TriliumNext/calendar
Add week note and quarter note support
This commit is contained in:
commit
a92b040958
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -13,10 +13,14 @@ This pattern works well also because of [Cloning Notes](../../Basic%20Concepts%2
|
||||
|
||||

|
||||
|
||||
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.
|
@ -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"> → {Promise.<<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"> → {Promise.<<a href="FNote.html">FNote</a>>}</span></h4>
|
||||
|
||||
|
||||
|
||||
|
@ -563,7 +563,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
* @param {string} date - e.g. "2019-04-29"
|
||||
* @returns {Promise<FNote>}
|
||||
*/
|
||||
this.getWeekNote = dateNotesService.getWeekNote;
|
||||
this.getWeekFirstDayNote = dateNotesService.getWeekFirstDayNote;
|
||||
|
||||
/**
|
||||
* Returns month-note. If it doesn't exist, it is automatically created.
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
62
src/public/app/doc_notes/en/User Guide/!!!meta.json
generated
62
src/public/app/doc_notes/en/User Guide/!!!meta.json
generated
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -21,19 +21,18 @@
|
||||
<h2>Alternatives</h2>
|
||||
<ul>
|
||||
<li>Pressing Ctrl+F while in a browser while not focused in a <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a> or
|
||||
a <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> 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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_iPIMuisry3hd">Text</a> note
|
||||
will reveal <a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/1YeN2MzFUluU/_help_MI26XDLSAlCD">CKEditor</a>'s
|
||||
href="#root/_help_iPIMuisry3hd">Text</a> or a <a class="reference-link"
|
||||
href="#root/_help_6f9hih2hXXZk">Code</a> 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 <a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a> note
|
||||
will reveal <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 <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_8YBEPzcpUgxw">Note buttons</a>,
|
||||
<li>From the <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>
|
||||
|
@ -24,10 +24,10 @@
|
||||
results as sub-items.</p>
|
||||
<h2>Accessing the search</h2>
|
||||
<ul>
|
||||
<li>From the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
||||
<li>From the <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 <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/oPVyFC7WL2Lp/_help_YtSN43OrfzaA">Note tree contextual menu</a> or
|
||||
the <a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note tree contextual menu</a> 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 & 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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_m523cpzocqaD">Saved Search</a>.</li>
|
||||
For more information, see <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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a> note
|
||||
<li>This feature allows writing a <a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a> 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> will
|
||||
<li><a class="reference-link" href="#root/_help_MKmLg5x6xkor">Archived Notes</a> 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
|
||||
<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 <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_ivYnonVFBxbQ">Bulk Actions</a>,
|
||||
which is an alternative for operating directly with notes within the
|
||||
<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 <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 & Execute actions</em> to trigger the
|
||||
actions.</li>
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
@ -334,7 +334,6 @@ body.layout-horizontal > .horizontal {
|
||||
}
|
||||
|
||||
.calendar-dropdown-widget .calendar-header {
|
||||
padding: 8px 0 20px 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
@ -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": "自动备份",
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -72,6 +72,8 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"locale",
|
||||
"formattingLocale",
|
||||
"firstDayOfWeek",
|
||||
"firstWeekOfYear",
|
||||
"minDaysInFirstWeek",
|
||||
"languages",
|
||||
"textNoteEditorType",
|
||||
"textNoteEditorMultilineToolbar",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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" },
|
||||
|
114
src/services/date_notes.spec.ts
Normal file
114
src/services/date_notes.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -85,6 +85,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
||||
eraseUnusedAttachmentsAfterSeconds: number;
|
||||
eraseUnusedAttachmentsAfterTimeScale: number;
|
||||
firstDayOfWeek: number;
|
||||
firstWeekOfYear: number;
|
||||
minDaysInFirstWeek: number;
|
||||
languages: string;
|
||||
|
||||
// Appearance
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -174,6 +174,7 @@
|
||||
"saturday": "周六",
|
||||
"sunday": "周日"
|
||||
},
|
||||
"weekdayNumber": "第 {weekNumber} 周",
|
||||
"months": {
|
||||
"january": "一月",
|
||||
"february": "二月",
|
||||
@ -188,6 +189,7 @@
|
||||
"november": "十一月",
|
||||
"december": "十二月"
|
||||
},
|
||||
"quarterNumber": "第 {quarterNumber} 季度",
|
||||
"special_notes": {
|
||||
"search_prefix": "搜索:"
|
||||
},
|
||||
|
@ -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:"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user