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();
+ }
+
}