From f2c88ee891c014a126664b57eec93ea38f593f4b Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 24 Aug 2026 22:11:16 +0200 Subject: [PATCH] Extract calendar draft helpers --- tests/static-toolboxes.test.mjs | 6 +- tests/toolbox-modules.test.mjs | 21 ++++++ .../toolboxes/modules/CalendarModule.jsx | 74 +++---------------- .../toolboxes/modules/calendarDraft.js | 74 +++++++++++++++++++ 4 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 website/src/features/toolboxes/modules/calendarDraft.js diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index c6a6241..8b13fdd 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -155,9 +155,10 @@ test("toolbox module registry and modules expose expected behavior", async () => const timerParts = await readFile("website/src/features/toolboxes/modules/TimerParts.jsx", "utf8"); const timerOptions = await readFile("website/src/features/toolboxes/modules/timerOptions.js", "utf8"); const calendarModule = await readFile("website/src/features/toolboxes/modules/CalendarModule.jsx", "utf8"); + const calendarDraft = await readFile("website/src/features/toolboxes/modules/calendarDraft.js", "utf8"); const calendarEventForm = await readFile("website/src/features/toolboxes/modules/CalendarEventForm.jsx", "utf8"); const calendarViews = await readFile("website/src/features/toolboxes/modules/CalendarViews.jsx", "utf8"); - const calendarBundle = `${calendarModule}\n${calendarEventForm}\n${calendarViews}`; + const calendarBundle = `${calendarModule}\n${calendarDraft}\n${calendarEventForm}\n${calendarViews}`; const calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8"); @@ -354,6 +355,9 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calendarModule, /type="calendar"/); assert.match(calendarBundle, /repeatLabel/); assert.match(calendarBundle, /CalendarAlertModeSwitch/); + assert.match(calendarDraft, /export function createDefaultDraft/); + assert.match(calendarDraft, /export function getDraftSelection/); + assert.match(calendarDraft, /export function normalizeDraftAlertOffsets/); assert.doesNotMatch(moduleRegistry, /calendarInitOpen/); assert.doesNotMatch(moduleRegistry, /calendar-init-modal/); assert.match(calendarUtils, /export function getCalendarOccurrences/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index b499166..002f826 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -6,6 +6,7 @@ import { exportModuleText, importModuleText, parseColonImportLines } from "../we import { buildInsertedMixTagValue, normalizeMixTagSpacing, normalizeMixTagText, removeMixTagAtOffset, tokenizeMixTagValue } from "../website/src/utils/mixTagsInputUtils.js"; import { getEffectiveGoalTreeProgressMode, getVisibleGoalTreeEdges } from "../website/src/features/toolboxes/modules/goalTreeDisplay.js"; import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js"; +import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftAlertOffsets, normalizeDraftMonthlyRepeatBy } from "../website/src/features/toolboxes/modules/calendarDraft.js"; import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, layoutConcurrentOccurrences } from "../website/src/features/toolboxes/modules/calendarLayout.js"; import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js"; import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; @@ -168,6 +169,26 @@ test("calendar layout keeps week compaction and overlapping lanes deterministic" ]); }); +test("calendar draft helpers normalize selections and options", () => { + const draft = createDefaultDraft(new Date(2026, 7, 14, 8, 17)); + assert.equal(draft.dayIndex, 4); + assert.equal(draft.startDate, "2026-08-14"); + assert.equal(draft.startTime, "08:30"); + assert.equal(draft.endTime, "09:00"); + + assert.deepEqual(getDraftSelection({ ...draft, startTime: "08:30", endTime: "10:00" }), { + dayIndex: 4, + startSlot: 17, + endSlot: 19 + }); + assert.equal(getDraftSelection({ ...draft, dayIndex: 8 }), null); + assert.deepEqual(normalizeDraftAlertOffsets([10, 5, 10, "60", 999]), [10, 5]); + assert.equal(normalizeDraftMonthlyRepeatBy("weekday"), "weekday"); + assert.equal(normalizeDraftMonthlyRepeatBy("invalid"), "monthDay"); + assert.equal(getDateSpanDays("2026-08-14", "2026-08-17"), 3); + assert.equal(getDateSpanDays("2026-08-17", "2026-08-14"), 0); +}); + test("calendar month layout splits bars on week boundaries and shares lanes", () => { const days = Array.from({ length: 14 }, (_, index) => ({ date: new Date(2026, 0, 5 + index), diff --git a/website/src/features/toolboxes/modules/CalendarModule.jsx b/website/src/features/toolboxes/modules/CalendarModule.jsx index 75dc398..f9e9e33 100644 --- a/website/src/features/toolboxes/modules/CalendarModule.jsx +++ b/website/src/features/toolboxes/modules/CalendarModule.jsx @@ -6,9 +6,7 @@ import { CalendarMonthView, CalendarWeekGrid } from "./CalendarViews.jsx"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; import { CALENDAR_ALERT_MODES, - CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, - CALENDAR_MONTHLY_REPEAT_BY, WEEK_SLOT_MINUTES, addDays, addMonths, @@ -24,68 +22,16 @@ import { parseLocalDateKey, timeValueToMinutes } from "./calendarUtils.js"; +import { WEEK_SLOT_COUNT, getWeekGridLayout } from "./calendarLayout.js"; import { - WEEK_SLOT_COUNT, - getWeekGridLayout -} from "./calendarLayout.js"; - -function createDefaultDraft(date = new Date()) { - const dayIndex = getLocalDayIndex(date); - const dateKey = formatLocalDateKey(date); - const startMinutes = Math.min(23 * 60 + 30, Math.ceil((date.getHours() * 60 + date.getMinutes()) / WEEK_SLOT_MINUTES) * WEEK_SLOT_MINUTES); - return { - id: "", - title: "", - dayIndex, - endDayIndex: dayIndex, - startDate: dateKey, - endDate: dateKey, - monthlyRepeatBy: "monthDay", - repeat: "monthly", - startTime: minutesToTimeValue(startMinutes), - endTime: minutesToTimeValue(Math.min(23 * 60 + 59, startMinutes + WEEK_SLOT_MINUTES)), - repeatable: true, - alertMode: "off", - alertOffsetMinutes: [], - color: "violet" - }; -} - -function getDraftSelection(draft) { - const dayIndex = Number(draft.dayIndex); - const startMinutes = timeValueToMinutes(draft.startTime); - const endMinutes = draft.endTime ? timeValueToMinutes(draft.endTime) : startMinutes + WEEK_SLOT_MINUTES; - if (dayIndex < 0 || dayIndex > 6 || startMinutes < 0 || endMinutes <= startMinutes) return null; - const startSlot = Math.floor(startMinutes / WEEK_SLOT_MINUTES); - const endSlot = Math.max(startSlot, Math.ceil(endMinutes / WEEK_SLOT_MINUTES) - 1); - return { - dayIndex, - startSlot: Math.max(0, Math.min(WEEK_SLOT_COUNT - 1, startSlot)), - endSlot: Math.max(0, Math.min(WEEK_SLOT_COUNT - 1, endSlot)) - }; -} - -function getCalendarRepeatMode() { - return "weekly"; -} - -function getDateSpanDays(startDateKey, endDateKey) { - const start = parseLocalDateKey(startDateKey); - const end = parseLocalDateKey(endDateKey); - if (!start || !end || end < start) return 0; - return Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)); -} - -function normalizeDraftDateKey(value) { - const date = parseLocalDateKey(value); - return date ? formatLocalDateKey(date) : ""; -} - -function normalizeDraftAlertOffsets(value) { - const rawOffsets = Array.isArray(value) ? value : [value]; - const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))]; - return offsets.sort((a, b) => b - a); -} + createDefaultDraft, + getCalendarRepeatMode, + getDateSpanDays, + getDraftSelection, + normalizeDraftAlertOffsets, + normalizeDraftDateKey, + normalizeDraftMonthlyRepeatBy +} from "./calendarDraft.js"; export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, textExchangeOpen }) { const data = context.normalizeCalendarData(context.getModuleData(toolboxId, moduleId, { initialized: false, viewMode: "week", events: [] })); @@ -330,7 +276,7 @@ export function CalendarModule({ toolboxId, moduleId, context, editing = false, const endDate = normalizeDraftDateKey(draft.endDate || draft.startDate); const startMinutes = data.viewMode === "month" ? 0 : timeValueToMinutes(draft.startTime); const endMinutes = data.viewMode === "month" ? -1 : draft.endTime ? timeValueToMinutes(draft.endTime) : -1; - const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY.includes(draft.monthlyRepeatBy) ? draft.monthlyRepeatBy : "monthDay"; + const monthlyRepeatBy = normalizeDraftMonthlyRepeatBy(draft.monthlyRepeatBy); const monthRepeat = monthlyRepeatBy === "monthDay" && draft.repeat === "weekly" ? "none" : ["none", "weekly", "monthly"].includes(draft.repeat) ? draft.repeat : "monthly"; const validWeekTarget = data.viewMode === "week" && dayIndex >= 0 && dayIndex <= 6; const validMonthTarget = data.viewMode === "month" && startDate && endDate && endDate >= startDate; diff --git a/website/src/features/toolboxes/modules/calendarDraft.js b/website/src/features/toolboxes/modules/calendarDraft.js new file mode 100644 index 0000000..e1178ad --- /dev/null +++ b/website/src/features/toolboxes/modules/calendarDraft.js @@ -0,0 +1,74 @@ +// Rôle : regroupe les helpers purs de draft et de sélection du calendrier. +import { + CALENDAR_ALERT_OFFSETS, + CALENDAR_MONTHLY_REPEAT_BY, + WEEK_SLOT_MINUTES, + formatLocalDateKey, + getLocalDayIndex, + minutesToTimeValue, + parseLocalDateKey, + timeValueToMinutes +} from "./calendarUtils.js"; +import { WEEK_SLOT_COUNT } from "./calendarLayout.js"; + +export function createDefaultDraft(date = new Date()) { + const dayIndex = getLocalDayIndex(date); + const dateKey = formatLocalDateKey(date); + const startMinutes = Math.min(23 * 60 + 30, Math.ceil((date.getHours() * 60 + date.getMinutes()) / WEEK_SLOT_MINUTES) * WEEK_SLOT_MINUTES); + return { + id: "", + title: "", + dayIndex, + endDayIndex: dayIndex, + startDate: dateKey, + endDate: dateKey, + monthlyRepeatBy: "monthDay", + repeat: "monthly", + startTime: minutesToTimeValue(startMinutes), + endTime: minutesToTimeValue(Math.min(23 * 60 + 59, startMinutes + WEEK_SLOT_MINUTES)), + repeatable: true, + alertMode: "off", + alertOffsetMinutes: [], + color: "violet" + }; +} + +export function getDraftSelection(draft) { + const dayIndex = Number(draft.dayIndex); + const startMinutes = timeValueToMinutes(draft.startTime); + const endMinutes = draft.endTime ? timeValueToMinutes(draft.endTime) : startMinutes + WEEK_SLOT_MINUTES; + if (dayIndex < 0 || dayIndex > 6 || startMinutes < 0 || endMinutes <= startMinutes) return null; + const startSlot = Math.floor(startMinutes / WEEK_SLOT_MINUTES); + const endSlot = Math.max(startSlot, Math.ceil(endMinutes / WEEK_SLOT_MINUTES) - 1); + return { + dayIndex, + startSlot: Math.max(0, Math.min(WEEK_SLOT_COUNT - 1, startSlot)), + endSlot: Math.max(0, Math.min(WEEK_SLOT_COUNT - 1, endSlot)) + }; +} + +export function getCalendarRepeatMode() { + return "weekly"; +} + +export function getDateSpanDays(startDateKey, endDateKey) { + const start = parseLocalDateKey(startDateKey); + const end = parseLocalDateKey(endDateKey); + if (!start || !end || end < start) return 0; + return Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)); +} + +export function normalizeDraftDateKey(value) { + const date = parseLocalDateKey(value); + return date ? formatLocalDateKey(date) : ""; +} + +export function normalizeDraftAlertOffsets(value) { + const rawOffsets = Array.isArray(value) ? value : [value]; + const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))]; + return offsets.sort((a, b) => b - a); +} + +export function normalizeDraftMonthlyRepeatBy(value) { + return CALENDAR_MONTHLY_REPEAT_BY.includes(value) ? value : "monthDay"; +}