From 27168b0dc5ecec02ac535b5da192d0fa4d79b9d3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 13 Feb 2025 22:57:30 +0200 Subject: [PATCH 01/30] feat(view/calendar): add calendar entry to views --- src/public/app/widgets/ribbon_widgets/book_properties.ts | 3 ++- src/public/translations/en/translation.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/ribbon_widgets/book_properties.ts b/src/public/app/widgets/ribbon_widgets/book_properties.ts index 6bb66dd1f..1a0671e3a 100644 --- a/src/public/app/widgets/ribbon_widgets/book_properties.ts +++ b/src/public/app/widgets/ribbon_widgets/book_properties.ts @@ -23,6 +23,7 @@ const TPL = ` @@ -125,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget { return; } - if (type !== "list" && type !== "grid") { + if (![ "list", "grid", "calendar"].includes(type)) { throw new Error(t("book_properties.invalid_view_type", { type })); } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 8825780fa..45eec36c5 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -751,7 +751,8 @@ "collapse": "Collapse", "expand": "Expand", "book_properties": "Book Properties", - "invalid_view_type": "Invalid view type '{{type}}'" + "invalid_view_type": "Invalid view type '{{type}}'", + "calendar": "Calendar" }, "edited_notes": { "no_edited_notes_found": "No edited notes on this day yet...", From e2bbee8e1629aac5aa1be4dbd99154fb4e2bbc43 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 13 Feb 2025 23:23:18 +0200 Subject: [PATCH 02/30] refactor(client): move implementation out of note_list_renderer --- src/public/app/services/note_list_renderer.ts | 399 +---------------- .../widgets/view_widgets/list_or_grid_view.ts | 406 ++++++++++++++++++ .../app/widgets/view_widgets/view_mode.ts | 11 + 3 files changed, 423 insertions(+), 393 deletions(-) create mode 100644 src/public/app/widgets/view_widgets/list_or_grid_view.ts create mode 100644 src/public/app/widgets/view_widgets/view_mode.ts diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index 06d2c1a48..6fd9d2ec7 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -1,404 +1,17 @@ -import linkService from "./link.js"; -import contentRenderer from "./content_renderer.js"; -import froca from "./froca.js"; -import attributeRenderer from "./attribute_renderer.js"; -import libraryLoader from "./library_loader.js"; -import treeService from "./tree.js"; -import utils from "./utils.js"; import type FNote from "../entities/fnote.js"; +import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; +import type ViewMode from "../widgets/view_widgets/view_mode.js"; -const TPL = ` -
- - -
-
- - - -
-
-
`; - -class NoteListRenderer { - private $noteList: JQuery; - - private parentNote: FNote; - private noteIds: string[]; - private page?: number; - private pageSize?: number; - private viewType?: string | null; - private showNotePath?: boolean; - private highlightRegex?: RegExp | null; - - /* - * We're using noteIds so that it's not necessary to load all notes at once when paging - */ constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - this.$noteList = $(TPL); - - // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work - $parent.empty(); - - this.parentNote = parentNote; - const includedNoteIds = this.getIncludedNoteIds(); - - this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); - - if (this.noteIds.length === 0) { - return; - } - - $parent.append(this.$noteList); - - this.page = 1; - this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); - - if (!this.pageSize || this.pageSize < 1) { - this.pageSize = 20; - } - - this.viewType = parentNote.getLabelValue("viewType"); - - if (!["list", "grid"].includes(this.viewType || "")) { - // when not explicitly set, decide based on the note type - this.viewType = parentNote.type === "search" ? "list" : "grid"; - } - - this.$noteList.addClass(`${this.viewType}-view`); - - this.showNotePath = showNotePath; - } - - /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which - * don't have to be shown in the note list. */ - getIncludedNoteIds() { - const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : []; - - return new Set(includedLinks.map((rel) => rel.value)); + this.viewMode = new ListOrGridView($parent, parentNote, noteIds, showNotePath); } async renderList() { - if (this.noteIds.length === 0 || !this.page || !this.pageSize) { - this.$noteList.hide(); - return; - } - - const highlightedTokens = this.parentNote.highlightedTokens || []; - if (highlightedTokens.length > 0) { - await libraryLoader.requireLibrary(libraryLoader.MARKJS); - - const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); - - this.highlightRegex = new RegExp(regex, "gi"); - } else { - this.highlightRegex = null; - } - - this.$noteList.show(); - - const $container = this.$noteList.find(".note-list-container").empty(); - - const startIdx = (this.page - 1) * this.pageSize; - const endIdx = startIdx + this.pageSize; - - const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); - const pageNotes = await froca.getNotes(pageNoteIds); - - for (const note of pageNotes) { - const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded")); - - $container.append($card); - } - - this.renderPager(); - - return this.$noteList; + return await this.viewMode.renderList(); } - renderPager() { - const $pager = this.$noteList.find(".note-list-pager").empty(); - if (!this.page || !this.pageSize) { - return; - } - - const pageCount = Math.ceil(this.noteIds.length / this.pageSize); - - $pager.toggle(pageCount > 1); - - let lastPrinted; - - for (let i = 1; i <= pageCount; i++) { - if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) { - lastPrinted = true; - - const startIndex = (i - 1) * this.pageSize + 1; - const endIndex = Math.min(this.noteIds.length, i * this.pageSize); - - $pager.append( - i === this.page - ? $("").text(i).css("text-decoration", "underline").css("font-weight", "bold") - : $('') - .text(i) - .attr("title", `Page of ${startIndex} - ${endIndex}`) - .on("click", () => { - this.page = i; - this.renderList(); - }), - "   " - ); - } else if (lastPrinted) { - $pager.append("...   "); - - lastPrinted = false; - } - } - - // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all - $pager.append(`(${this.noteIds.length} notes)`); - } - - async renderNote(note: FNote, expand: boolean = false) { - const $expander = $(''); - - const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); - const notePath = - this.parentNote.type === "search" - ? note.noteId // for search note parent, we want to display a non-search path - : `${this.parentNote.noteId}/${note.noteId}`; - - const $card = $('
') - .attr("data-note-id", note.noteId) - .append( - $('
') - .append($expander) - .append($('').addClass(note.getIcon())) - .append( - this.viewType === "grid" - ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) - : (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title") - ) - .append($renderedAttributes) - ); - - if (this.viewType === "grid") { - $card - .addClass("block-link") - .attr("data-href", `#${notePath}`) - .on("click", (e) => linkService.goToLink(e)); - } - - $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); - - if (this.highlightRegex) { - $card.find(".note-book-title").markRegExp(this.highlightRegex, { - element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false - }); - } - - await this.toggleContent($card, note, expand); - - return $card; - } - - async toggleContent($card: JQuery, note: FNote, expand: boolean) { - if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { - return; - } - - const $expander = $card.find("> .note-book-header .note-expander"); - - if (expand || this.viewType === "grid") { - $card.addClass("expanded"); - $expander.addClass("bx-chevron-down").removeClass("bx-chevron-right"); - } else { - $card.removeClass("expanded"); - $expander.addClass("bx-chevron-right").removeClass("bx-chevron-down"); - } - - if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) { - $card.append(await this.renderNoteContent(note)); - } - } - - async renderNoteContent(note: FNote) { - const $content = $('
'); - - try { - const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, { - trim: this.viewType === "grid" // for grid only short content is needed - }); - - if (this.highlightRegex) { - $renderedContent.markRegExp(this.highlightRegex, { - element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false - }); - } - - $content.append($renderedContent); - $content.addClass(`type-${type}`); - } catch (e) { - console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); - console.error(e); - - $content.append("rendering error"); - } - - if (this.viewType === "list") { - const imageLinks = note.getRelations("imageLink"); - - const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId)); - - for (const childNote of childNotes) { - $content.append(await this.renderNote(childNote)); - } - } - - return $content; - } } - -export default NoteListRenderer; diff --git a/src/public/app/widgets/view_widgets/list_or_grid_view.ts b/src/public/app/widgets/view_widgets/list_or_grid_view.ts new file mode 100644 index 000000000..96d4037a4 --- /dev/null +++ b/src/public/app/widgets/view_widgets/list_or_grid_view.ts @@ -0,0 +1,406 @@ +import linkService from "../../services/link.js"; +import contentRenderer from "../../services/content_renderer.js"; +import froca from "../../services/froca.js"; +import attributeRenderer from "../../services/attribute_renderer.js"; +import libraryLoader from "../../services/library_loader.js"; +import treeService from "../../services/tree.js"; +import utils from "../../services/utils.js"; +import type FNote from "../../entities/fnote.js"; +import ViewMode from "./view_mode.js"; + +const TPL = ` +
+ + +
+
+ + + +
+
+
`; + +class ListOrGridView extends ViewMode { + private $noteList: JQuery; + + private parentNote: FNote; + private noteIds: string[]; + private page?: number; + private pageSize?: number; + private viewType?: string | null; + private showNotePath?: boolean; + private highlightRegex?: RegExp | null; + + /* + * We're using noteIds so that it's not necessary to load all notes at once when paging + */ + constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + super($parent, parentNote, noteIds, showNotePath); + this.$noteList = $(TPL); + + // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work + $parent.empty(); + + this.parentNote = parentNote; + const includedNoteIds = this.getIncludedNoteIds(); + + this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); + + if (this.noteIds.length === 0) { + return; + } + + $parent.append(this.$noteList); + + this.page = 1; + this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); + + if (!this.pageSize || this.pageSize < 1) { + this.pageSize = 20; + } + + this.viewType = parentNote.getLabelValue("viewType"); + + if (!["list", "grid"].includes(this.viewType || "")) { + // when not explicitly set, decide based on the note type + this.viewType = parentNote.type === "search" ? "list" : "grid"; + } + + this.$noteList.addClass(`${this.viewType}-view`); + + this.showNotePath = showNotePath; + } + + /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which + * don't have to be shown in the note list. */ + getIncludedNoteIds() { + const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : []; + + return new Set(includedLinks.map((rel) => rel.value)); + } + + async renderList() { + if (this.noteIds.length === 0 || !this.page || !this.pageSize) { + this.$noteList.hide(); + return; + } + + const highlightedTokens = this.parentNote.highlightedTokens || []; + if (highlightedTokens.length > 0) { + await libraryLoader.requireLibrary(libraryLoader.MARKJS); + + const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); + + this.highlightRegex = new RegExp(regex, "gi"); + } else { + this.highlightRegex = null; + } + + this.$noteList.show(); + + const $container = this.$noteList.find(".note-list-container").empty(); + + const startIdx = (this.page - 1) * this.pageSize; + const endIdx = startIdx + this.pageSize; + + const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); + const pageNotes = await froca.getNotes(pageNoteIds); + + for (const note of pageNotes) { + const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded")); + + $container.append($card); + } + + this.renderPager(); + + return this.$noteList; + } + + renderPager() { + const $pager = this.$noteList.find(".note-list-pager").empty(); + if (!this.page || !this.pageSize) { + return; + } + + const pageCount = Math.ceil(this.noteIds.length / this.pageSize); + + $pager.toggle(pageCount > 1); + + let lastPrinted; + + for (let i = 1; i <= pageCount; i++) { + if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) { + lastPrinted = true; + + const startIndex = (i - 1) * this.pageSize + 1; + const endIndex = Math.min(this.noteIds.length, i * this.pageSize); + + $pager.append( + i === this.page + ? $("").text(i).css("text-decoration", "underline").css("font-weight", "bold") + : $('
') + .text(i) + .attr("title", `Page of ${startIndex} - ${endIndex}`) + .on("click", () => { + this.page = i; + this.renderList(); + }), + "   " + ); + } else if (lastPrinted) { + $pager.append("...   "); + + lastPrinted = false; + } + } + + // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all + $pager.append(`(${this.noteIds.length} notes)`); + } + + async renderNote(note: FNote, expand: boolean = false) { + const $expander = $(''); + + const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); + const notePath = + this.parentNote.type === "search" + ? note.noteId // for search note parent, we want to display a non-search path + : `${this.parentNote.noteId}/${note.noteId}`; + + const $card = $('
') + .attr("data-note-id", note.noteId) + .append( + $('
') + .append($expander) + .append($('').addClass(note.getIcon())) + .append( + this.viewType === "grid" + ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) + : (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title") + ) + .append($renderedAttributes) + ); + + if (this.viewType === "grid") { + $card + .addClass("block-link") + .attr("data-href", `#${notePath}`) + .on("click", (e) => linkService.goToLink(e)); + } + + $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); + + if (this.highlightRegex) { + $card.find(".note-book-title").markRegExp(this.highlightRegex, { + element: "span", + className: "ck-find-result", + separateWordSearch: false, + caseSensitive: false + }); + } + + await this.toggleContent($card, note, expand); + + return $card; + } + + async toggleContent($card: JQuery, note: FNote, expand: boolean) { + if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { + return; + } + + const $expander = $card.find("> .note-book-header .note-expander"); + + if (expand || this.viewType === "grid") { + $card.addClass("expanded"); + $expander.addClass("bx-chevron-down").removeClass("bx-chevron-right"); + } else { + $card.removeClass("expanded"); + $expander.addClass("bx-chevron-right").removeClass("bx-chevron-down"); + } + + if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) { + $card.append(await this.renderNoteContent(note)); + } + } + + async renderNoteContent(note: FNote) { + const $content = $('
'); + + try { + const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, { + trim: this.viewType === "grid" // for grid only short content is needed + }); + + if (this.highlightRegex) { + $renderedContent.markRegExp(this.highlightRegex, { + element: "span", + className: "ck-find-result", + separateWordSearch: false, + caseSensitive: false + }); + } + + $content.append($renderedContent); + $content.addClass(`type-${type}`); + } catch (e) { + console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); + console.error(e); + + $content.append("rendering error"); + } + + if (this.viewType === "list") { + const imageLinks = note.getRelations("imageLink"); + + const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId)); + + for (const childNote of childNotes) { + $content.append(await this.renderNote(childNote)); + } + } + + return $content; + } +} + +export default ListOrGridView; diff --git a/src/public/app/widgets/view_widgets/view_mode.ts b/src/public/app/widgets/view_widgets/view_mode.ts new file mode 100644 index 000000000..c854bd37f --- /dev/null +++ b/src/public/app/widgets/view_widgets/view_mode.ts @@ -0,0 +1,11 @@ +import type FNote from "../../entities/fnote.js"; + +export default abstract class ViewMode { + + constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + + } + + abstract renderList(): Promise | undefined>; + +} From 4592d6750b15bf5adb40a93ad293ec01cc7e5605 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 13 Feb 2025 23:46:20 +0200 Subject: [PATCH 03/30] feat(view/calendar): render a text in calendar view --- src/public/app/services/note_list_renderer.ts | 31 +++++++++++++++-- .../app/widgets/view_widgets/calendar_view.ts | 33 +++++++++++++++++++ .../widgets/view_widgets/list_or_grid_view.ts | 13 ++------ .../app/widgets/view_widgets/view_mode.ts | 3 +- 4 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 src/public/app/widgets/view_widgets/calendar_view.ts diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index 6fd9d2ec7..4229889a8 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -1,16 +1,43 @@ import type FNote from "../entities/fnote.js"; +import CalendarView from "../widgets/view_widgets/calendar_view.js"; import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; import type ViewMode from "../widgets/view_widgets/view_mode.js"; export default class NoteListRenderer { - private viewMode: ViewMode; + private viewType: string; + private viewMode: ViewMode | null; constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - this.viewMode = new ListOrGridView($parent, parentNote, noteIds, showNotePath); + console.log("Parent note is ", parentNote); + this.viewType = this.#getViewType(parentNote); + console.log("View type is ", this.viewType); + + if (this.viewType === "list" || this.viewType === "grid") { + this.viewMode = new ListOrGridView(this.viewType, $parent, parentNote, noteIds, showNotePath); + } else if (this.viewType === "calendar") { + this.viewMode = new CalendarView(this.viewType, $parent, parentNote, noteIds, showNotePath); + } else { + this.viewMode = null; + } + } + + #getViewType(parentNote: FNote): string { + const viewType = parentNote.getLabelValue("viewType"); + + if (!["list", "grid", "calendar"].includes(viewType || "")) { + // when not explicitly set, decide based on the note type + return parentNote.type === "search" ? "list" : "grid"; + } else { + return viewType as string; + } } async renderList() { + if (!this.viewMode) { + return null; + } + return await this.viewMode.renderList(); } diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts new file mode 100644 index 000000000..6ae367735 --- /dev/null +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -0,0 +1,33 @@ +import type FNote from "../../entities/fnote.js"; +import ViewMode from "./view_mode.js"; + +const TPL = ` +
+ + + Hello world. +
+`; + +export default class CalendarView extends ViewMode { + + private $root: JQuery; + + constructor(viewType: string, $parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + super($parent, parentNote, noteIds, showNotePath); + + this.$root = $(TPL); + $parent.append(this.$root); + } + + async renderList(): Promise | undefined> { + return this.$root; + } + +} diff --git a/src/public/app/widgets/view_widgets/list_or_grid_view.ts b/src/public/app/widgets/view_widgets/list_or_grid_view.ts index 96d4037a4..b5ebc5e02 100644 --- a/src/public/app/widgets/view_widgets/list_or_grid_view.ts +++ b/src/public/app/widgets/view_widgets/list_or_grid_view.ts @@ -172,12 +172,10 @@ class ListOrGridView extends ViewMode { /* * We're using noteIds so that it's not necessary to load all notes at once when paging */ - constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + constructor(viewType: string, $parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { super($parent, parentNote, noteIds, showNotePath); this.$noteList = $(TPL); - - // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work - $parent.empty(); + this.viewType = viewType; this.parentNote = parentNote; const includedNoteIds = this.getIncludedNoteIds(); @@ -197,13 +195,6 @@ class ListOrGridView extends ViewMode { this.pageSize = 20; } - this.viewType = parentNote.getLabelValue("viewType"); - - if (!["list", "grid"].includes(this.viewType || "")) { - // when not explicitly set, decide based on the note type - this.viewType = parentNote.type === "search" ? "list" : "grid"; - } - this.$noteList.addClass(`${this.viewType}-view`); this.showNotePath = showNotePath; diff --git a/src/public/app/widgets/view_widgets/view_mode.ts b/src/public/app/widgets/view_widgets/view_mode.ts index c854bd37f..31dfee8fd 100644 --- a/src/public/app/widgets/view_widgets/view_mode.ts +++ b/src/public/app/widgets/view_widgets/view_mode.ts @@ -3,7 +3,8 @@ import type FNote from "../../entities/fnote.js"; export default abstract class ViewMode { constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - + // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work + $parent.empty(); } abstract renderList(): Promise | undefined>; From 68ccd23540d6ed93b8274a9407d0bbca77c279f7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:13:47 +0200 Subject: [PATCH 04/30] refactor(views): pass argument to constructor --- src/public/app/services/note_list_renderer.ts | 13 +++++++++---- .../app/widgets/view_widgets/calendar_view.ts | 8 ++++---- .../widgets/view_widgets/list_or_grid_view.ts | 16 ++++++++-------- src/public/app/widgets/view_widgets/view_mode.ts | 11 +++++++++-- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index 4229889a8..48a618598 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -1,6 +1,7 @@ import type FNote from "../entities/fnote.js"; import CalendarView from "../widgets/view_widgets/calendar_view.js"; import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; +import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; import type ViewMode from "../widgets/view_widgets/view_mode.js"; export default class NoteListRenderer { @@ -9,14 +10,18 @@ export default class NoteListRenderer { private viewMode: ViewMode | null; constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - console.log("Parent note is ", parentNote); this.viewType = this.#getViewType(parentNote); - console.log("View type is ", this.viewType); + const args: ViewModeArgs = { + $parent, + parentNote, + noteIds, + showNotePath + } if (this.viewType === "list" || this.viewType === "grid") { - this.viewMode = new ListOrGridView(this.viewType, $parent, parentNote, noteIds, showNotePath); + this.viewMode = new ListOrGridView(this.viewType, args); } else if (this.viewType === "calendar") { - this.viewMode = new CalendarView(this.viewType, $parent, parentNote, noteIds, showNotePath); + this.viewMode = new CalendarView(args); } else { this.viewMode = null; } diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 6ae367735..7f02b7537 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,5 +1,5 @@ import type FNote from "../../entities/fnote.js"; -import ViewMode from "./view_mode.js"; +import ViewMode, { type ViewModeArgs } from "./view_mode.js"; const TPL = `
@@ -19,11 +19,11 @@ export default class CalendarView extends ViewMode { private $root: JQuery; - constructor(viewType: string, $parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - super($parent, parentNote, noteIds, showNotePath); + constructor(args: ViewModeArgs) { + super(args); this.$root = $(TPL); - $parent.append(this.$root); + args.$parent.append(this.$root); } async renderList(): Promise | undefined> { diff --git a/src/public/app/widgets/view_widgets/list_or_grid_view.ts b/src/public/app/widgets/view_widgets/list_or_grid_view.ts index b5ebc5e02..56ee00b86 100644 --- a/src/public/app/widgets/view_widgets/list_or_grid_view.ts +++ b/src/public/app/widgets/view_widgets/list_or_grid_view.ts @@ -6,7 +6,7 @@ import libraryLoader from "../../services/library_loader.js"; import treeService from "../../services/tree.js"; import utils from "../../services/utils.js"; import type FNote from "../../entities/fnote.js"; -import ViewMode from "./view_mode.js"; +import ViewMode, { type ViewModeArgs } from "./view_mode.js"; const TPL = `
@@ -172,24 +172,24 @@ class ListOrGridView extends ViewMode { /* * We're using noteIds so that it's not necessary to load all notes at once when paging */ - constructor(viewType: string, $parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - super($parent, parentNote, noteIds, showNotePath); + constructor(viewType: string, args: ViewModeArgs) { + super(args); this.$noteList = $(TPL); this.viewType = viewType; - this.parentNote = parentNote; + this.parentNote = args.parentNote; const includedNoteIds = this.getIncludedNoteIds(); - this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); + this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); if (this.noteIds.length === 0) { return; } - $parent.append(this.$noteList); + args.$parent.append(this.$noteList); this.page = 1; - this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); + this.pageSize = parseInt(args.parentNote.getLabelValue("pageSize") || ""); if (!this.pageSize || this.pageSize < 1) { this.pageSize = 20; @@ -197,7 +197,7 @@ class ListOrGridView extends ViewMode { this.$noteList.addClass(`${this.viewType}-view`); - this.showNotePath = showNotePath; + this.showNotePath = args.showNotePath; } /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which diff --git a/src/public/app/widgets/view_widgets/view_mode.ts b/src/public/app/widgets/view_widgets/view_mode.ts index 31dfee8fd..df61b7977 100644 --- a/src/public/app/widgets/view_widgets/view_mode.ts +++ b/src/public/app/widgets/view_widgets/view_mode.ts @@ -1,10 +1,17 @@ import type FNote from "../../entities/fnote.js"; +export interface ViewModeArgs { + $parent: JQuery; + parentNote: FNote; + noteIds: string[]; + showNotePath?: boolean; +} + export default abstract class ViewMode { - constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + constructor(args: ViewModeArgs) { // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work - $parent.empty(); + args.$parent.empty(); } abstract renderList(): Promise | undefined>; From 462a2713a27301a1622537d768e1089a7a7b1d67 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:23:33 +0200 Subject: [PATCH 05/30] feat(calendar_view): integrate fullcalendar --- package-lock.json | 30 +++++++++++++++++++ package.json | 2 ++ .../app/widgets/view_widgets/calendar_view.ts | 15 ++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fdf64248..6ecbf9a9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@braintree/sanitize-url": "7.1.1", "@electron/remote": "2.1.2", "@excalidraw/excalidraw": "0.17.6", + "@fullcalendar/core": "6.1.15", + "@fullcalendar/daygrid": "6.1.15", "@highlightjs/cdn-assets": "11.11.1", "@joplin/turndown-plugin-gfm": "1.0.61", "@mermaid-js/layout-elk": "0.1.7", @@ -2125,6 +2127,34 @@ "react-dom": "^17.0.2 || ^18.2.0" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", + "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", + "license": "MIT", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/core/node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz", + "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/package.json b/package.json index 9829a639f..d44202f15 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "@braintree/sanitize-url": "7.1.1", "@electron/remote": "2.1.2", "@excalidraw/excalidraw": "0.17.6", + "@fullcalendar/core": "6.1.15", + "@fullcalendar/daygrid": "6.1.15", "@highlightjs/cdn-assets": "11.11.1", "@joplin/turndown-plugin-gfm": "1.0.61", "@mermaid-js/layout-elk": "0.1.7", diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 7f02b7537..e1a2c43cb 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,4 +1,3 @@ -import type FNote from "../../entities/fnote.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; const TPL = ` @@ -11,22 +10,34 @@ const TPL = ` } - Hello world. +
+
`; export default class CalendarView extends ViewMode { private $root: JQuery; + private $calendarContainer: JQuery; constructor(args: ViewModeArgs) { super(args); this.$root = $(TPL); + this.$calendarContainer = this.$root.find(".calendar-container"); args.$parent.append(this.$root); } async renderList(): Promise | undefined> { + const { Calendar } = await import("@fullcalendar/core"); + const dayGridPlugin = (await import("@fullcalendar/daygrid")).default; + + const calendar = new Calendar(this.$calendarContainer[0], { + plugins: [ dayGridPlugin ], + initialView: "dayGridMonth" + }); + calendar.render(); + return this.$root; } From fd22d05d9bc6ddfe6e506dfc2a3f86a67d173077 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:23:57 +0200 Subject: [PATCH 06/30] feat(calendar_view): disable user selection --- src/public/app/widgets/view_widgets/calendar_view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index e1a2c43cb..63e50088f 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -7,6 +7,7 @@ const TPL = ` overflow: hidden; position: relative; height: 100%; + user-select: none; } From 557e4cdfea36872a6816e1a3cfb6ef6d4ed2ab35 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:24:40 +0200 Subject: [PATCH 07/30] style(calendar_view): disable link color --- src/public/app/widgets/view_widgets/calendar_view.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 63e50088f..c6c6281ab 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -9,6 +9,10 @@ const TPL = ` height: 100%; user-select: none; } + + .calendar-view a { + color: unset; + }
From 4dc2803cc68f6bc8a11c786d5a576f8a8b1a5d84 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:35:14 +0200 Subject: [PATCH 08/30] style(calendar_view): apply a padding --- src/public/app/widgets/view_widgets/calendar_view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index c6c6281ab..0747f2fc5 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -8,6 +8,7 @@ const TPL = ` position: relative; height: 100%; user-select: none; + padding: 10px; } .calendar-view a { From 64b446315d430112206ec405f6b0225e4fc50c25 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:43:46 +0200 Subject: [PATCH 09/30] feat(calendar_view): render notes by their start date --- .../app/widgets/view_widgets/calendar_view.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 0747f2fc5..b222e86d5 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,3 +1,5 @@ +import type { EventSourceInput } from "@fullcalendar/core"; +import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; const TPL = ` @@ -25,12 +27,14 @@ export default class CalendarView extends ViewMode { private $root: JQuery; private $calendarContainer: JQuery; + private noteIds: string[]; constructor(args: ViewModeArgs) { super(args); this.$root = $(TPL); this.$calendarContainer = this.$root.find(".calendar-container"); + this.noteIds = args.noteIds; args.$parent.append(this.$root); } @@ -40,11 +44,32 @@ export default class CalendarView extends ViewMode { const calendar = new Calendar(this.$calendarContainer[0], { plugins: [ dayGridPlugin ], - initialView: "dayGridMonth" + initialView: "dayGridMonth", + events: await CalendarView.#buildEvents(this.noteIds) }); calendar.render(); return this.$root; } + static async #buildEvents(noteIds: string[]) { + const notes = await froca.getNotes(noteIds); + const events: EventSourceInput = []; + + for (const note of notes) { + const startDate = note.getAttributeValue("label", "startDate"); + + if (!startDate) { + continue; + } + + events.push({ + title: note.title, + start: startDate + }); + } + + return events; + } + } From 8111352300e2fe6c9a6539e09df5ecf24a8b345b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 10:58:12 +0200 Subject: [PATCH 10/30] feat(calendar_view): render notes by their end date --- .../app/widgets/view_widgets/calendar_view.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index b222e86d5..af0a54b4a 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -58,15 +58,23 @@ export default class CalendarView extends ViewMode { for (const note of notes) { const startDate = note.getAttributeValue("label", "startDate"); - if (!startDate) { continue; } - events.push({ + const eventData: typeof events[0] = { title: note.title, start: startDate - }); + }; + + const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); + if (endDate) { + // Fullcalendar end date is exclusive, not inclusive. + endDate.setDate(endDate.getDate() + 1); + eventData.end = endDate.toISOString().substring(0, 10); + } + + events.push(eventData); } return events; From 10b2d1971092a67873def5e32858fafeaf13aa4c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 11:13:44 +0200 Subject: [PATCH 11/30] feat(calendar_view): allow rendering by custom attribute --- .../app/widgets/view_widgets/calendar_view.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index af0a54b4a..2dcebc9e5 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,6 +1,7 @@ import type { EventSourceInput } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; +import type FNote from "../../entities/fnote.js"; const TPL = `
@@ -58,12 +59,14 @@ export default class CalendarView extends ViewMode { for (const note of notes) { const startDate = note.getAttributeValue("label", "startDate"); + const customTitle = note.getAttributeValue("label", "calendar:title"); + if (!startDate) { continue; } const eventData: typeof events[0] = { - title: note.title, + title: CalendarView.#parseCustomTitle(customTitle, note), start: startDate }; @@ -80,4 +83,15 @@ export default class CalendarView extends ViewMode { return events; } + static #parseCustomTitle(customTitleValue: string | null, note: FNote) { + if (customTitleValue && customTitleValue.startsWith("#")) { + const labelValue = note.getAttributeValue("label", customTitleValue.substring(1)); + if (labelValue) { + return labelValue; + } + } + + return note.title; + } + } From a8509d8b1d353a4fa780444ebd54308450ff9558 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 11:41:08 +0200 Subject: [PATCH 12/30] feat(calendar_view): allow rendering by custom relation --- .../app/widgets/view_widgets/calendar_view.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 2dcebc9e5..2d3fda3c8 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -66,7 +66,7 @@ export default class CalendarView extends ViewMode { } const eventData: typeof events[0] = { - title: CalendarView.#parseCustomTitle(customTitle, note), + title: (await CalendarView.#parseCustomTitle(customTitle, note))[0], start: startDate }; @@ -83,15 +83,33 @@ export default class CalendarView extends ViewMode { return events; } - static #parseCustomTitle(customTitleValue: string | null, note: FNote) { - if (customTitleValue && customTitleValue.startsWith("#")) { - const labelValue = note.getAttributeValue("label", customTitleValue.substring(1)); - if (labelValue) { - return labelValue; + static async #parseCustomTitle(customTitleValue: string | null, note: FNote, allowRelations = true): Promise { + if (customTitleValue) { + const attributeName = customTitleValue.substring(1); + if (customTitleValue.startsWith("#")) { + const labelValue = note.getAttributeValue("label", attributeName); + if (labelValue) { + return [ labelValue ]; + } + } else if (allowRelations && customTitleValue.startsWith("~")) { + const relations = note.getRelations(attributeName); + if (relations.length > 0) { + const noteIds = relations.map((r) => r.targetNoteId); + const notesFromRelation = await froca.getNotes(noteIds); + const titles = []; + + for (const targetNote of notesFromRelation) { + const targetCustomTitleValue = targetNote.getAttributeValue("label", "calendar:title"); + const targetTitles = await CalendarView.#parseCustomTitle(targetCustomTitleValue, targetNote, false); + titles.push(targetTitles.flat()); + } + + return titles.flat(); + } } } - return note.title; + return [ note.title ]; } } From 1b4281cf77e8abf2b36e53350779dd34ef726f48 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 11:42:06 +0200 Subject: [PATCH 13/30] feat(calendar_view): allow rendering multiple events from multiple relation --- .../app/widgets/view_widgets/calendar_view.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 2d3fda3c8..8c3a8e4a6 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -65,19 +65,22 @@ export default class CalendarView extends ViewMode { continue; } - const eventData: typeof events[0] = { - title: (await CalendarView.#parseCustomTitle(customTitle, note))[0], - start: startDate - }; + const titles = await CalendarView.#parseCustomTitle(customTitle, note); + for (const title of titles) { + const eventData: typeof events[0] = { + title: title, + start: startDate + }; - const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); - if (endDate) { - // Fullcalendar end date is exclusive, not inclusive. - endDate.setDate(endDate.getDate() + 1); - eventData.end = endDate.toISOString().substring(0, 10); + const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); + if (endDate) { + // Fullcalendar end date is exclusive, not inclusive. + endDate.setDate(endDate.getDate() + 1); + eventData.end = endDate.toISOString().substring(0, 10); + } + + events.push(eventData); } - - events.push(eventData); } return events; From 23cbc82708ebd2f3c934e5c3cfaf140734f53963 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 11:46:17 +0200 Subject: [PATCH 14/30] feat(calendar_view): clickable events --- src/public/app/widgets/view_widgets/calendar_view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 8c3a8e4a6..f485f763c 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -69,7 +69,8 @@ export default class CalendarView extends ViewMode { for (const title of titles) { const eventData: typeof events[0] = { title: title, - start: startDate + start: startDate, + url: `#${note.noteId}` }; const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); From e71f0cb091cfea01a348e5b9181458e7f21cc8a4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 12:05:35 +0200 Subject: [PATCH 15/30] feat(calendar_view): draggable events --- package-lock.json | 10 +++ package.json | 1 + .../app/widgets/view_widgets/calendar_view.ts | 61 +++++++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ecbf9a9b..123acb09e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@excalidraw/excalidraw": "0.17.6", "@fullcalendar/core": "6.1.15", "@fullcalendar/daygrid": "6.1.15", + "@fullcalendar/interaction": "6.1.15", "@highlightjs/cdn-assets": "11.11.1", "@joplin/turndown-plugin-gfm": "1.0.61", "@mermaid-js/layout-elk": "0.1.7", @@ -2155,6 +2156,15 @@ "@fullcalendar/core": "~6.1.15" } }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz", + "integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/package.json b/package.json index d44202f15..ad05b0216 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@excalidraw/excalidraw": "0.17.6", "@fullcalendar/core": "6.1.15", "@fullcalendar/daygrid": "6.1.15", + "@fullcalendar/interaction": "6.1.15", "@highlightjs/cdn-assets": "11.11.1", "@joplin/turndown-plugin-gfm": "1.0.61", "@mermaid-js/layout-elk": "0.1.7", diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index f485f763c..31572e2f3 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,7 +1,9 @@ -import type { EventSourceInput } from "@fullcalendar/core"; +import type { EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; +import server from "../../services/server.js"; +import ws from "../../services/ws.js"; const TPL = `
@@ -40,13 +42,47 @@ export default class CalendarView extends ViewMode { } async renderList(): Promise | undefined> { + const editable = true; + const { Calendar } = await import("@fullcalendar/core"); - const dayGridPlugin = (await import("@fullcalendar/daygrid")).default; + const plugins: PluginDef[] = []; + plugins.push((await import("@fullcalendar/daygrid")).default); + + if (editable) { + plugins.push((await import("@fullcalendar/interaction")).default); + } const calendar = new Calendar(this.$calendarContainer[0], { - plugins: [ dayGridPlugin ], + plugins, initialView: "dayGridMonth", - events: await CalendarView.#buildEvents(this.noteIds) + events: await CalendarView.#buildEvents(this.noteIds), + editable, + eventDragStop: async (e) => { + const startDate = e.event.start?.toISOString().substring(0, 10); + let endDate = e.event.end?.toISOString().substring(0, 10); + const noteId = e.event.extendedProps.noteId; + + // Fullcalendar end date is exclusive, not inclusive but we store it the other way around. + if (endDate) { + const endDateParsed = new Date(endDate); + endDateParsed.setDate(endDateParsed.getDate() - 1); + endDate = endDateParsed.toISOString().substring(0, 10); + } + + // Don't store the end date if it's empty. + if (endDate === startDate) { + endDate = undefined; + } + + // Update start date + const note = await froca.getNote(noteId); + if (!note) { + return; + } + + CalendarView.#setAttribute(note, "label", "startDate", startDate); + CalendarView.#setAttribute(note, "label", "endDate", endDate); + } }); calendar.render(); @@ -70,7 +106,8 @@ export default class CalendarView extends ViewMode { const eventData: typeof events[0] = { title: title, start: startDate, - url: `#${note.noteId}` + url: `#${note.noteId}`, + noteId: note.noteId }; const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); @@ -116,4 +153,18 @@ export default class CalendarView extends ViewMode { return [ note.title ]; } + static async #setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { + if (value) { + // Create or update the attribute. + await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); + } else { + // Remove the attribute if it exists on the server but we don't define a value for it. + const attributeId = note.getAttribute(type, name); + if (attributeId) { + await server.remove(`notes/${note.noteId}/attributes/${attributeId}`); + } + } + await ws.waitForMaxKnownEntityChangeId(); + } + } From 22c8cf3beefb684629b448fdd67075b23f2a7d99 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 12:26:58 +0200 Subject: [PATCH 16/30] feat(calendar_view): allow resizing & fix date offset --- .../app/widgets/view_widgets/calendar_view.ts | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 31572e2f3..0a6ef4df5 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,9 +1,10 @@ -import type { EventSourceInput, PluginDef } from "@fullcalendar/core"; +import type { EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; import server from "../../services/server.js"; import ws from "../../services/ws.js"; +import type { EventDragStopArg, EventResizeDoneArg } from "@fullcalendar/interaction"; const TPL = `
@@ -57,38 +58,40 @@ export default class CalendarView extends ViewMode { initialView: "dayGridMonth", events: await CalendarView.#buildEvents(this.noteIds), editable, - eventDragStop: async (e) => { - const startDate = e.event.start?.toISOString().substring(0, 10); - let endDate = e.event.end?.toISOString().substring(0, 10); - const noteId = e.event.extendedProps.noteId; - - // Fullcalendar end date is exclusive, not inclusive but we store it the other way around. - if (endDate) { - const endDateParsed = new Date(endDate); - endDateParsed.setDate(endDateParsed.getDate() - 1); - endDate = endDateParsed.toISOString().substring(0, 10); - } - - // Don't store the end date if it's empty. - if (endDate === startDate) { - endDate = undefined; - } - - // Update start date - const note = await froca.getNote(noteId); - if (!note) { - return; - } - - CalendarView.#setAttribute(note, "label", "startDate", startDate); - CalendarView.#setAttribute(note, "label", "endDate", endDate); - } + eventChange: (e) => this.#onEventMoved(e), }); calendar.render(); return this.$root; } + async #onEventMoved(e: EventChangeArg) { + const startDate = CalendarView.#formatDateToLocalISO(e.event.start); + let endDate = CalendarView.#formatDateToLocalISO(e.event.end); + const noteId = e.event.extendedProps.noteId; + + // Fullcalendar end date is exclusive, not inclusive but we store it the other way around. + if (endDate) { + const endDateParsed = new Date(endDate); + endDateParsed.setDate(endDateParsed.getDate() - 1); + endDate = CalendarView.#formatDateToLocalISO(endDateParsed); + } + + // Don't store the end date if it's empty. + if (endDate === startDate) { + endDate = undefined; + } + + // Update start date + const note = await froca.getNote(noteId); + if (!note) { + return; + } + + CalendarView.#setAttribute(note, "label", "startDate", startDate); + CalendarView.#setAttribute(note, "label", "endDate", endDate); + } + static async #buildEvents(noteIds: string[]) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; @@ -114,7 +117,7 @@ export default class CalendarView extends ViewMode { if (endDate) { // Fullcalendar end date is exclusive, not inclusive. endDate.setDate(endDate.getDate() + 1); - eventData.end = endDate.toISOString().substring(0, 10); + eventData.end = CalendarView.#formatDateToLocalISO(endDate); } events.push(eventData); @@ -159,7 +162,7 @@ export default class CalendarView extends ViewMode { await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); } else { // Remove the attribute if it exists on the server but we don't define a value for it. - const attributeId = note.getAttribute(type, name); + const attributeId = note.getAttribute(type, name)?.attributeId; if (attributeId) { await server.remove(`notes/${note.noteId}/attributes/${attributeId}`); } @@ -167,4 +170,14 @@ export default class CalendarView extends ViewMode { await ws.waitForMaxKnownEntityChangeId(); } + static #formatDateToLocalISO(date: Date | null | undefined) { + if (!date) { + return undefined; + } + + const offset = date.getTimezoneOffset(); + const localDate = new Date(date.getTime() - offset * 60 * 1000); + return localDate.toISOString().split('T')[0]; + } + } From 660a393ca65989c5306261be51e1e9c1cac9a9dd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 14:07:39 +0200 Subject: [PATCH 17/30] fix(calendar_view): double scrollbar --- src/public/app/widgets/view_widgets/calendar_view.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 0a6ef4df5..af168d55b 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -20,6 +20,10 @@ const TPL = ` .calendar-view a { color: unset; } + + .calendar-container { + height: 100%; + }
From 03282e12b7a2c853c20782950446b0ade3c87475 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 14:15:43 +0200 Subject: [PATCH 18/30] style(calendar_view): improve layout slightly --- .../app/widgets/view_widgets/calendar_view.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index af168d55b..75e812703 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -24,6 +24,19 @@ const TPL = ` .calendar-container { height: 100%; } + + .calendar-container .fc-toolbar.fc-header-toolbar { + margin-bottom: 0.5em; + } + + .calendar-container .fc-toolbar-title { + font-size: 1.3rem; + font-weight: normal; + } + + .calendar-container .fc-button { + padding: 0.2em 0.5em; + }
From 0bcabacd3adf413531073a450315ce831bbc7c5d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 20:15:54 +0200 Subject: [PATCH 19/30] feat(calendar_view): translate buttons --- src/public/app/widgets/view_widgets/calendar_view.ts | 8 ++++++++ src/public/translations/en/translation.json | 7 +++++++ src/public/translations/ro/translation.json | 10 +++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 75e812703..f2dbaa971 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -5,6 +5,7 @@ import type FNote from "../../entities/fnote.js"; import server from "../../services/server.js"; import ws from "../../services/ws.js"; import type { EventDragStopArg, EventResizeDoneArg } from "@fullcalendar/interaction"; +import { t } from "../../services/i18n.js"; const TPL = `
@@ -76,6 +77,13 @@ export default class CalendarView extends ViewMode { events: await CalendarView.#buildEvents(this.noteIds), editable, eventChange: (e) => this.#onEventMoved(e), + buttonText: { + today: t("calendar_view.today"), + month: t("calendar_view.month"), + week: t("calendar_view.week"), + day: t("calendar_view.day"), + list: t("calendar_view.list") + } }); calendar.render(); diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 45eec36c5..e9ff9d146 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1655,5 +1655,12 @@ "minutes": "Minutes", "hours": "Hours", "days": "Days" + }, + "calendar_view": { + "today": "today", + "month": "month", + "week": "week", + "day": "day", + "list": "list" } } diff --git a/src/public/translations/ro/translation.json b/src/public/translations/ro/translation.json index eabe682d1..ca35fe475 100644 --- a/src/public/translations/ro/translation.json +++ b/src/public/translations/ro/translation.json @@ -281,7 +281,8 @@ "grid": "Grilă", "invalid_view_type": "Mod de afișare incorect „{{type}}”", "list": "Listă", - "view_type": "Mod de afișare" + "view_type": "Mod de afișare", + "calendar": "Calendar" }, "bookmark_switch": { "bookmark": "Semn de carte", @@ -1657,5 +1658,12 @@ }, "help-button": { "title": "Deschide ghidul relevant" + }, + "calendar_view": { + "day": "zi", + "list": "listă", + "month": "lună", + "today": "azi", + "week": "săptămână" } } From 9d9b45cafb4ab3a51175cfb41b64d49269e144b4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 20:18:27 +0200 Subject: [PATCH 20/30] feat(calendar_view): set first day of the week based on option --- src/public/app/widgets/view_widgets/calendar_view.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index f2dbaa971..ebb987839 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -4,8 +4,8 @@ import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; import server from "../../services/server.js"; import ws from "../../services/ws.js"; -import type { EventDragStopArg, EventResizeDoneArg } from "@fullcalendar/interaction"; import { t } from "../../services/i18n.js"; +import options from "../../services/options.js"; const TPL = `
@@ -83,7 +83,8 @@ export default class CalendarView extends ViewMode { week: t("calendar_view.week"), day: t("calendar_view.day"), list: t("calendar_view.list") - } + }, + firstDay: options.getInt("firstDayOfWeek") ?? 0 }); calendar.render(); From f8c3587717410fa1e597385ca6ba67d465267150 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 21:45:53 +0200 Subject: [PATCH 21/30] feat(view/calendar): add locale support --- .../app/widgets/view_widgets/calendar_view.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index ebb987839..3d477b26b 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -84,13 +84,37 @@ export default class CalendarView extends ViewMode { day: t("calendar_view.day"), list: t("calendar_view.list") }, - firstDay: options.getInt("firstDayOfWeek") ?? 0 + firstDay: options.getInt("firstDayOfWeek") ?? 0, + locale: await CalendarView.#getLocale() }); calendar.render(); return this.$root; } + static async #getLocale() { + const locale = options.get("locale"); + + // Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages. + switch (locale) { + case "de": + return (await import("@fullcalendar/core/locales/de")).default; + case "es": + return (await import("@fullcalendar/core/locales/es")).default; + case "fr": + return (await import("@fullcalendar/core/locales/fr")).default; + case "cn": + return (await import("@fullcalendar/core/locales/zh-cn")).default; + case "tw": + return (await import("@fullcalendar/core/locales/zh-tw")).default; + case "ro": + return (await import("@fullcalendar/core/locales/ro")).default; + case "en": + default: + return undefined; + } + } + async #onEventMoved(e: EventChangeArg) { const startDate = CalendarView.#formatDateToLocalISO(e.event.start); let endDate = CalendarView.#formatDateToLocalISO(e.event.end); From b524abb883a138db5a169795fd5a8c75afa2e1c3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 21:47:44 +0200 Subject: [PATCH 22/30] feat(view/calendar): use locale translation for buttons --- src/public/app/widgets/view_widgets/calendar_view.ts | 7 ------- src/public/translations/en/translation.json | 7 ------- src/public/translations/ro/translation.json | 7 ------- 3 files changed, 21 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 3d477b26b..92daed922 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -77,13 +77,6 @@ export default class CalendarView extends ViewMode { events: await CalendarView.#buildEvents(this.noteIds), editable, eventChange: (e) => this.#onEventMoved(e), - buttonText: { - today: t("calendar_view.today"), - month: t("calendar_view.month"), - week: t("calendar_view.week"), - day: t("calendar_view.day"), - list: t("calendar_view.list") - }, firstDay: options.getInt("firstDayOfWeek") ?? 0, locale: await CalendarView.#getLocale() }); diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index e9ff9d146..45eec36c5 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1655,12 +1655,5 @@ "minutes": "Minutes", "hours": "Hours", "days": "Days" - }, - "calendar_view": { - "today": "today", - "month": "month", - "week": "week", - "day": "day", - "list": "list" } } diff --git a/src/public/translations/ro/translation.json b/src/public/translations/ro/translation.json index ca35fe475..445b657bc 100644 --- a/src/public/translations/ro/translation.json +++ b/src/public/translations/ro/translation.json @@ -1658,12 +1658,5 @@ }, "help-button": { "title": "Deschide ghidul relevant" - }, - "calendar_view": { - "day": "zi", - "list": "listă", - "month": "lună", - "today": "azi", - "week": "săptămână" } } From aa5c69d97ab74e4d8210fbcb0f3b6a39dd9b20ca Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Feb 2025 23:48:06 +0200 Subject: [PATCH 23/30] feat(view/calendar): add flow to create a note --- .../app/widgets/view_widgets/calendar_view.ts | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 92daed922..22f7b9d52 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,4 +1,4 @@ -import type { EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; +import type { DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; @@ -6,6 +6,8 @@ import server from "../../services/server.js"; import ws from "../../services/ws.js"; import { t } from "../../services/i18n.js"; import options from "../../services/options.js"; +import dialogService from "../../services/dialog.js"; +import attributes from "../../services/attributes.js"; const TPL = `
@@ -45,11 +47,19 @@ const TPL = `
`; +// TODO: Deduplicate +interface CreateChildResponse { + note: { + noteId: string; + } +} + export default class CalendarView extends ViewMode { private $root: JQuery; private $calendarContainer: JQuery; private noteIds: string[]; + private parentNote: FNote; constructor(args: ViewModeArgs) { super(args); @@ -57,17 +67,19 @@ export default class CalendarView extends ViewMode { this.$root = $(TPL); this.$calendarContainer = this.$root.find(".calendar-container"); this.noteIds = args.noteIds; + this.parentNote = args.parentNote; + console.log(args); args.$parent.append(this.$root); } async renderList(): Promise | undefined> { - const editable = true; + const isEditable = true; const { Calendar } = await import("@fullcalendar/core"); const plugins: PluginDef[] = []; plugins.push((await import("@fullcalendar/daygrid")).default); - if (editable) { + if (isEditable) { plugins.push((await import("@fullcalendar/interaction")).default); } @@ -75,7 +87,9 @@ export default class CalendarView extends ViewMode { plugins, initialView: "dayGridMonth", events: await CalendarView.#buildEvents(this.noteIds), - editable, + editable: isEditable, + selectable: isEditable, + select: (e) => this.#onCalendarSelection(e), eventChange: (e) => this.#onEventMoved(e), firstDay: options.getInt("firstDayOfWeek") ?? 0, locale: await CalendarView.#getLocale() @@ -108,9 +122,33 @@ export default class CalendarView extends ViewMode { } } + async #onCalendarSelection(e: DateSelectArg) { + const startDate = CalendarView.#formatDateToLocalISO(e.start); + if (!startDate) { + return; + } + + const endDate = CalendarView.#formatDateToLocalISO(CalendarView.#offsetDate(e.end, -1)); + + const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); + if (!title?.trim()) { + return; + } + + const { note } = await server.post(`notes/${this.parentNote.noteId}/children?target=into`, { + title, + content: "", + type: "text" + }); + attributes.setLabel(note.noteId, "startDate", startDate); + if (endDate) { + attributes.setLabel(note.noteId, "endDate", endDate); + } + } + async #onEventMoved(e: EventChangeArg) { const startDate = CalendarView.#formatDateToLocalISO(e.event.start); - let endDate = CalendarView.#formatDateToLocalISO(e.event.end); + let endDate = CalendarView.#formatDateToLocalISO(CalendarView.#offsetDate(e.event.end, -1)); const noteId = e.event.extendedProps.noteId; // Fullcalendar end date is exclusive, not inclusive but we store it the other way around. @@ -156,7 +194,7 @@ export default class CalendarView extends ViewMode { noteId: note.noteId }; - const endDate = new Date(note.getAttributeValue("label", "endDate") ?? startDate); + const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, -1); if (endDate) { // Fullcalendar end date is exclusive, not inclusive. endDate.setDate(endDate.getDate() + 1); @@ -223,4 +261,14 @@ export default class CalendarView extends ViewMode { return localDate.toISOString().split('T')[0]; } + static #offsetDate(date: Date | string | null | undefined, offset: number) { + if (!date) { + return undefined; + } + + const newDate = new Date(date); + newDate.setDate(newDate.getDate() + offset); + return newDate; + } + } From 6d3325766d0f8f4c162798b6aeabe9635421089a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 16 Feb 2025 13:22:44 +0200 Subject: [PATCH 24/30] feat(view/calendar): refresh calendar when entities change --- src/public/app/services/note_list_renderer.ts | 2 +- src/public/app/widgets/note_list.ts | 11 +++++++++-- .../app/widgets/view_widgets/calendar_view.ts | 18 ++++++++++++++++-- .../app/widgets/view_widgets/view_mode.ts | 5 +++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index 48a618598..16aa0d7f2 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -7,7 +7,7 @@ import type ViewMode from "../widgets/view_widgets/view_mode.js"; export default class NoteListRenderer { private viewType: string; - private viewMode: ViewMode | null; + public viewMode: ViewMode | null; constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { this.viewType = this.#getViewType(parentNote); diff --git a/src/public/app/widgets/note_list.ts b/src/public/app/widgets/note_list.ts index 97fd19bef..b1cfd06d9 100644 --- a/src/public/app/widgets/note_list.ts +++ b/src/public/app/widgets/note_list.ts @@ -2,6 +2,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteListRenderer from "../services/note_list_renderer.js"; import type FNote from "../entities/fnote.js"; import type { EventData } from "../components/app_context.js"; +import type ViewMode from "./view_widgets/view_mode.js"; const TPL = `
@@ -26,6 +27,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { private isIntersecting?: boolean; private noteIdRefreshed?: string; private shownNoteId?: string | null; + private viewMode?: ViewMode | null; isEnabled() { return super.isEnabled() && this.noteContext?.hasNoteList(); @@ -67,6 +69,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { async renderNoteList(note: FNote) { const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds()); await noteListRenderer.renderList(); + this.viewMode = noteListRenderer.viewMode; } async refresh() { @@ -102,11 +105,15 @@ export default class NoteListWidget extends NoteContextAwareWidget { } } - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) { + entitiesReloadedEvent(e: EventData<"entitiesReloaded">) { + if (e.loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) { this.shownNoteId = null; // force render this.checkRenderStatus(); } + + if (this.viewMode) { + this.viewMode.entitiesReloadedEvents(e); + } } } diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 22f7b9d52..56fcaf50e 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,4 +1,4 @@ -import type { DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; +import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; @@ -8,6 +8,7 @@ import { t } from "../../services/i18n.js"; import options from "../../services/options.js"; import dialogService from "../../services/dialog.js"; import attributes from "../../services/attributes.js"; +import type { EventData } from "../../components/app_context.js"; const TPL = `
@@ -60,6 +61,7 @@ export default class CalendarView extends ViewMode { private $calendarContainer: JQuery; private noteIds: string[]; private parentNote: FNote; + private calendar?: Calendar; constructor(args: ViewModeArgs) { super(args); @@ -86,7 +88,7 @@ export default class CalendarView extends ViewMode { const calendar = new Calendar(this.$calendarContainer[0], { plugins, initialView: "dayGridMonth", - events: await CalendarView.#buildEvents(this.noteIds), + events: async () => await CalendarView.#buildEvents(this.noteIds), editable: isEditable, selectable: isEditable, select: (e) => this.#onCalendarSelection(e), @@ -95,6 +97,7 @@ export default class CalendarView extends ViewMode { locale: await CalendarView.#getLocale() }); calendar.render(); + this.calendar = calendar; return this.$root; } @@ -173,6 +176,17 @@ export default class CalendarView extends ViewMode { CalendarView.#setAttribute(note, "label", "endDate", endDate); } + entitiesReloadedEvents({ loadResults }: EventData<"entitiesReloaded">): void { + // Refresh note IDs if they got changed. + if (loadResults.getBranchRows().some((branch) => branch.parentNoteId == this.parentNote.noteId)) { + this.noteIds = this.parentNote.getChildNoteIds(); + } + + if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) { + this.calendar.refetchEvents(); + } + } + static async #buildEvents(noteIds: string[]) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; diff --git a/src/public/app/widgets/view_widgets/view_mode.ts b/src/public/app/widgets/view_widgets/view_mode.ts index df61b7977..e17d3cebf 100644 --- a/src/public/app/widgets/view_widgets/view_mode.ts +++ b/src/public/app/widgets/view_widgets/view_mode.ts @@ -1,3 +1,4 @@ +import type { EventData } from "../../components/app_context.js"; import type FNote from "../../entities/fnote.js"; export interface ViewModeArgs { @@ -16,4 +17,8 @@ export default abstract class ViewMode { abstract renderList(): Promise | undefined>; + entitiesReloadedEvents(e: EventData<"entitiesReloaded">) { + // Do nothing by default. + } + } From ee5cc18ef296464f76df3662bd5eac43efbdec48 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 16 Feb 2025 13:34:39 +0200 Subject: [PATCH 25/30] fix(view/calendar): end date offset on display --- src/public/app/widgets/view_widgets/calendar_view.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 56fcaf50e..d89642fba 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -208,10 +208,8 @@ export default class CalendarView extends ViewMode { noteId: note.noteId }; - const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, -1); + const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, 1); if (endDate) { - // Fullcalendar end date is exclusive, not inclusive. - endDate.setDate(endDate.getDate() + 1); eventData.end = CalendarView.#formatDateToLocalISO(endDate); } From c17d10114ff8bdf22adcd5c92222b405d07d500b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 16 Feb 2025 17:52:04 +0200 Subject: [PATCH 26/30] feat(mobile): enable prompt dialog for inserting notes --- src/public/app/layouts/mobile_layout.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/public/app/layouts/mobile_layout.ts b/src/public/app/layouts/mobile_layout.ts index a98b3df29..51609ce32 100644 --- a/src/public/app/layouts/mobile_layout.ts +++ b/src/public/app/layouts/mobile_layout.ts @@ -31,6 +31,7 @@ import type AppContext from "../components/app_context.js"; import TabRowWidget from "../widgets/tab_row.js"; import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js"; import RecentChangesDialog from "../widgets/dialogs/recent_changes.js"; +import PromptDialog from "../widgets/dialogs/prompt.js"; const MOBILE_CSS = `