Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-10-03 15:58:33 +02:00
parent acd9b2fe11
commit 808610dcf2
53 changed files with 842 additions and 329 deletions

View file

@ -248,6 +248,14 @@ export function validateSiteContent(site) {
"toolboxes.spaces.moveSuccess",
"toolboxes.spaces.emptyText",
"toolboxes.spaces.limitReached",
"toolboxes.moduleConfiguration.buttonLabel",
"toolboxes.moduleConfiguration.panelLabel",
"toolboxes.moduleConfiguration.organizationTitle",
"toolboxes.moduleConfiguration.spaceLabel",
"toolboxes.moduleConfiguration.guideTitle",
"toolboxes.moduleConfiguration.dataTitle",
"toolboxes.moduleConfiguration.importButtonLabel",
"toolboxes.moduleConfiguration.exportButtonLabel",
"toolboxes.drawer.emptyText",
"toolboxes.drawer.createLabel",
"toolboxes.drawer.linkExistingTitle",
@ -345,6 +353,12 @@ export function validateSiteContent(site) {
"toolboxes.modules.memos.tickerLabel",
"toolboxes.modules.memos.hideTickerTitle",
"toolboxes.modules.memos.showTickerTitle",
"toolboxes.modules.memos.settingsTitle",
"toolboxes.modules.memos.plannerLinkLabel",
"toolboxes.modules.memos.plannerNoneLabel",
"toolboxes.modules.memos.plannerAllLabel",
"toolboxes.modules.memos.plannerDefaultLabel",
"toolboxes.modules.memos.taskAlertTemplate",
"toolboxes.modules.memos.importPlaceholder",
"toolboxes.modules.memos.importOpenButton",
"toolboxes.modules.memos.exportOpenButton",
@ -830,7 +844,6 @@ export function validateSiteContent(site) {
"toolboxes.modules.calendar.requiredError",
"toolboxes.modules.calendar.invalidRangeError",
"toolboxes.modules.taskPlanner.settingsTitle",
"toolboxes.modules.taskPlanner.settingsButtonLabel",
"toolboxes.modules.taskPlanner.hideCompletedTitle",
"toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel",
"toolboxes.modules.taskPlanner.resetTimeLabel",

View file

@ -4,6 +4,7 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js";
import { applyDueResets, getTaskParentMap, getTaskTreeIds, isTaskResetSoon, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js";
import { getMemosDataAfterPlannerRemoval, getPlannerTaskTickerItems, resolveMemosPlannerLink } from "../website/src/features/toolboxes/modules/memosPlannerUtils.js";
import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js";
import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js";
import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js";
@ -97,7 +98,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const toolboxPages = await readFile("website/src/features/toolboxes/ToolboxPages.jsx", "utf8");
const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8");
const moduleShell = await readFile("website/src/features/toolboxes/modules/ModuleShell.jsx", "utf8");
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}\n${moduleShell}`;
const moduleConfiguration = await readFile("website/src/features/toolboxes/modules/ModuleConfiguration.jsx", "utf8");
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}\n${moduleShell}\n${moduleConfiguration}`;
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8");
@ -139,7 +141,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const taskPlannerReorder = await readFile("website/src/features/toolboxes/modules/useTaskPlannerReorder.js", "utf8");
const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8");
const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8");
const taskPlannerBundle = `${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`;
const taskPlannerBundle = `${taskPlannerModule}\n${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`;
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8");
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
@ -240,12 +242,18 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(moduleShell, /module-scroll-button/);
assert.match(moduleShell, /module-collapse-button/);
assert.match(moduleShell, /aria-expanded=\{!collapsed\}/);
assert.match(moduleShell, /!collapsed &&/);
assert.match(moduleShell, /!collapsed \|\| configurationOpen/);
assert.match(moduleShell, /module-configuration-button/);
assert.match(moduleShell, /aria-controls=\{configurationId\}/);
assert.match(moduleShell, /module-configuration-panel/);
assert.match(moduleShell, /hidden=\{collapsed\}/);
assert.match(moduleConfiguration, /createPortal/);
assert.doesNotMatch(moduleShell, /module-exchange-button/);
assert.match(addToolControls, /Icon name="dropdown"/);
assert.match(addToolControls, /CompactDropdown/);
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
assert.match(notepadModule, /export function NotepadModule/);
assert.match(notepadModule, /TextExchangeActions/);
assert.match(notepadModule, /ModuleTextExchangeConfiguration/);
assert.match(notepadModule, /type="notepad"/);
assert.match(notepadToolbar, /export function NotepadToolbar/);
assert.match(notepadToolbar, /FormattingColorDropdown/);
@ -300,7 +308,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(timerBundle, /useInlineEdit/);
assert.match(timerModule, /useDraftForm/);
assert.match(timerModule, /normalizeTimerData/);
assert.match(timerModule, /TextExchangeActions/);
assert.match(timerModule, /ModuleTextExchangeConfiguration/);
assert.match(calendarModule, /export function CalendarModule/);
assert.match(calendarModule, /normalizeCalendarData/);
assert.match(calendarBundle, /calendar-mode-choice/);
@ -322,7 +330,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(calendarBundle, /data-size=\{eventSurfaceSize\}/);
assert.match(calendarBundle, /is-compact/);
assert.match(calendarController, /handleKeyDown/);
assert.match(calendarContent, /TextExchangeActions/);
assert.match(calendarContent, /ModuleTextExchangeConfiguration/);
assert.match(calendarContent, /type="calendar"/);
assert.match(calendarBundle, /repeatLabel/);
assert.match(calendarBundle, /CalendarAlertModeSwitch/);
@ -342,7 +350,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(goalTreeModule, /export function GoalTreeModule/);
assert.match(goalTreeBundle, /normalizeGoalTreeData/);
assert.match(goalTreeBundle, /applyGoalTreeDueResets/);
assert.match(goalTreeModule, /TextExchangeActions/);
assert.match(goalTreeModule, /ModuleTextExchangeConfiguration/);
assert.match(goalTreeBundle, /DiagramConnectors/);
assert.match(goalTreeEditor, /export function GoalTreeEditor/);
assert.match(goalTreeEditor, /Tabs/);
@ -388,14 +396,14 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(equipmentPlannerEditors, /export \{ CharacteristicsEditor \}/);
assert.match(equipmentPlannerEditors, /export \{ MaterialsEditor \}/);
assert.match(equipmentPlannerEditors, /export \{ SocketItemsEditor \}/);
assert.match(equipmentPlannerModule, /TextExchangeActions/);
assert.match(equipmentPlannerModule, /ModuleTextExchangeConfiguration/);
assert.match(equipmentPlannerItem, /export function EquipmentItem/);
assert.match(equipmentPlannerPickers, /export function IconChoiceDropdown/);
assert.match(equipmentPlannerSummary, /export function EquipmentPlannerSummary/);
assert.match(equipmentPlannerOptions, /export const EQUIPMENT_ICONS/);
assert.match(equipmentPlannerOptions, /export function formatValue/);
assert.match(checklistModule, /export function ChecklistModule/);
assert.match(checklistModule, /TextExchangeActions/);
assert.match(checklistModule, /ModuleTextExchangeConfiguration/);
assert.match(checklistModule, /editing &&/);
assert.match(checklistParts, /ChecklistItem/);
assert.match(checklistParts, /export function ChecklistSection/);
@ -420,7 +428,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(imagesModule, /editing &&/);
assert.match(imagesModule, /clipboardData/);
assert.match(imagesModule, /createImageAnnotationModule/);
assert.match(imagesModule, /TextExchangeActions/);
assert.match(imagesModule, /ModuleTextExchangeConfiguration/);
assert.match(imagesModule, /image-annotate-button/);
assert.match(imagesModule, /kind: "collection"/);
assert.match(imagesModule, /galleryButton/);
@ -432,15 +440,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(imageAnnotationModule, /context\.setImage/);
assert.match(imageAnnotationModule, /createMarkerId/);
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
assert.match(imageAnnotationModule, /TextExchangeActions/);
assert.match(linksModule, /TextExchangeActions/);
assert.match(imageAnnotationModule, /ModuleTextExchangeConfiguration/);
assert.match(linksModule, /ModuleTextExchangeConfiguration/);
assert.match(linksModule, /normalizeUrl/);
assert.match(linksModule, /useGroupedReorder/);
assert.match(linksModule, /link-drag-handle/);
assert.match(countersModule, /useGroupedReorder/);
assert.match(countersModule, /orientation: "horizontal"/);
assert.match(countersModule, /counter-drag-handle/);
assert.match(countersModule, /TextExchangeActions/);
assert.match(countersModule, /ModuleTextExchangeConfiguration/);
assert.match(timerModule, /useGroupedReorder/);
assert.match(timerModule, /reorderEnabled={!data\.sortResults}/);
assert.match(timerModule, /parentId=\{moduleId\}/);
@ -510,6 +518,41 @@ test("task planner helpers keep hierarchy, reorder and reset behavior stable", (
assert.equal(isTaskResetSoon({ id: "h", title: "H", type: "unique", checked: false }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), false);
});
test("memos planner links select reset alerts and clear removed targets", () => {
const modules = [
{ id: "planner-a", type: "taskPlanner", title: "Raids" },
{ id: "planner-b", type: "taskPlanner", title: "Farm" },
{ id: "notes", type: "memos" }
];
const plannerData = {
"planner-a": {
resetTime: "06:00",
weeklyResetDay: 1,
tasks: [
{ id: "parent", title: "Parent", type: "unique", category: "Raid" },
{ id: "soon", title: "Donjon", type: "daily" },
{ id: "done", title: "Déjà faite", type: "daily", checked: true, checkedAt: Date.UTC(2026, 0, 2, 0) }
],
relations: [{ id: "relation", fromTaskId: "soon", toTaskId: "parent" }]
},
"planner-b": { resetTime: "18:00", tasks: [{ id: "later", title: "Récolte", type: "daily" }] }
};
const alerts = getPlannerTaskTickerItems({
plannerLink: "all",
modules,
getPlannerData: (plannerId) => plannerData[plannerId],
nowMs: Date.UTC(2026, 0, 2, 1),
alertTemplate: "Tâche : {category}{title} expire bientôt"
});
assert.deepEqual(alerts.map((item) => item.text), ["Tâche : Raid Donjon expire bientôt"]);
assert.equal(alerts[0].kind, "task-warning");
assert.equal(resolveMemosPlannerLink("missing", modules), "");
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "planner-a" }, "planner-a", modules.slice(1)).plannerLink, "");
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "all" }, "planner-a", modules.slice(1)).plannerLink, "all");
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "all" }, "planner-b", [{ id: "notes", type: "memos" }]).plannerLink, "");
});
test("equipment planner options expose stable formatting and socket helpers", () => {
assert.deepEqual(SOCKET_SHAPES, ["ball", "jewel"]);
assert.equal(formatValue(1.23456), "1.235");

View file

@ -14,10 +14,34 @@ import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } fro
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngRealDrawsSummary, calculateRngSummary, getAveragePositionStatus, getDurationPresetsForAverageHours, getEffectiveRate, normalizeCostString, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, remapMemosPlannerLink, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { createToolboxSpace, deleteToolboxSpace, moveToolboxModule, reorderToolboxSpaces } from "../website/src/features/toolboxes/toolboxSpaceUtils.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
test("memos persist and export a remapped task planner link", () => {
const normalized = normalizeMemosData({ plannerLink: "planner", items: [] });
assert.equal(normalized.plannerLink, "planner");
assert.equal(compactModuleDataForStorage("memos", normalized).plannerLink, "planner");
assert.equal(remapMemosPlannerLink({ plannerLink: "planner" }, (id) => `new-${id}`).plannerLink, "new-planner");
assert.equal(remapMemosPlannerLink({ plannerLink: "all" }, () => "unused").plannerLink, "all");
const toolbox = normalizeToolbox({
id: "toolbox",
name: "Linked planners",
modules: [
{ id: "memos", type: "memos" },
{ id: "planner", type: "taskPlanner" }
]
});
const payload = createToolboxExportPayload(toolbox, {
[moduleStorageKey(toolbox.id, "memos")]: { plannerLink: "planner" },
[moduleStorageKey(toolbox.id, "planner")]: { tasks: [] }
});
const exportedMemos = payload.toolbox.modules.find((module) => module.type === "memos");
const exportedPlanner = payload.toolbox.modules.find((module) => module.type === "taskPlanner");
assert.equal(payload.modules[exportedMemos.id].plannerLink, exportedPlanner.id);
});
function createTextImportContext() {
let index = 0;
return {