Add toolbox spaces and recover from stale asset bundles
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-09-29 21:31:49 +02:00
parent 0959c6740b
commit 0acf5ecf21
30 changed files with 1252 additions and 179 deletions

View file

@ -217,6 +217,24 @@ export function validateSiteContent(site) {
"toolboxes.storageStatus.errorMessage",
"toolboxes.storageStatus.retryLabel",
"toolboxes.storageStatus.writeErrorTitle",
"toolboxes.spaces.toolbarLabel",
"toolboxes.spaces.tabsLabel",
"toolboxes.spaces.addLabel",
"toolboxes.spaces.createTitle",
"toolboxes.spaces.renameTitle",
"toolboxes.spaces.renameLabel",
"toolboxes.spaces.deleteTitle",
"toolboxes.spaces.deleteLabel",
"toolboxes.spaces.deleteEmpty",
"toolboxes.spaces.deleteWithTools",
"toolboxes.spaces.nameLabel",
"toolboxes.spaces.cancelLabel",
"toolboxes.spaces.saveLabel",
"toolboxes.spaces.reorderLabel",
"toolboxes.spaces.moveLabel",
"toolboxes.spaces.moveSuccess",
"toolboxes.spaces.emptyText",
"toolboxes.spaces.limitReached",
"toolboxes.title",
"toolboxes.newButton",
"toolboxes.libraryLink",
@ -1150,15 +1168,20 @@ export function validateLibraryData(payload) {
moduleIds.add(module.id);
assert.equal(typeof payload.modules[module.id], "object", `library.modules.${module.id} must be an object`);
});
assert.deepEqual(payload.toolbox.moduleOrder?.one, [...moduleIds], "library.toolbox.moduleOrder.one must match toolbox modules");
assert.ok(Array.isArray(payload.toolbox.moduleOrder?.two), "library.toolbox.moduleOrder.two must be an array");
assert.equal(payload.toolbox.moduleOrder.two.length, 2, "library.toolbox.moduleOrder.two must contain 2 columns");
payload.toolbox.moduleOrder.two.forEach((column, index) => {
assert.ok(Array.isArray(column), `library.toolbox.moduleOrder.two[${index}] must be an array`);
assert.ok(Array.isArray(payload.toolbox.spaces), "library.toolbox.spaces must be an array");
assert.equal(payload.toolbox.spaces.length, 1, "library.toolbox must declare one default space");
const defaultSpace = payload.toolbox.spaces[0];
assert.equal(payload.toolbox.defaultSpaceId, defaultSpace.id, "library.toolbox.defaultSpaceId must target the default space");
assert.equal(payload.toolbox.activeSpaceId, defaultSpace.id, "library.toolbox.activeSpaceId must target the default space");
assert.deepEqual(defaultSpace.moduleOrder?.one, [...moduleIds], "library default space order must match toolbox modules");
assert.ok(Array.isArray(defaultSpace.moduleOrder?.two), "library default space two-column order must be an array");
assert.equal(defaultSpace.moduleOrder.two.length, 2, "library default space must contain 2 columns");
defaultSpace.moduleOrder.two.forEach((column, index) => {
assert.ok(Array.isArray(column), `library default space column ${index} must be an array`);
});
const twoColumnIds = payload.toolbox.moduleOrder.two.flat();
assert.deepEqual(new Set(twoColumnIds), moduleIds, "library.toolbox.moduleOrder.two must contain every module exactly once");
assert.equal(twoColumnIds.length, moduleIds.size, "library.toolbox.moduleOrder.two must not contain duplicate modules");
const twoColumnIds = defaultSpace.moduleOrder.two.flat();
assert.deepEqual(new Set(twoColumnIds), moduleIds, "library default space must contain every module exactly once");
assert.equal(twoColumnIds.length, moduleIds.size, "library default space must not contain duplicate modules");
["memos", "notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);

View file

@ -137,6 +137,32 @@ test("global imports started from the same render both survive", async () => {
assert.deepEqual(queue.getCurrent(), disk);
});
test("v2 imports reject more than five spaces before writing", async () => {
let writes = 0;
let errorMessage = "";
const actions = useToolboxActions({
store: {
ready: true,
persistToolboxSnapshot: () => { writes += 1; return Promise.resolve(true); }
},
notify() {},
setConfirmModal: (value) => { errorMessage = value.message; }
});
const result = await actions.importToolbox({ text: async () => JSON.stringify({
version: 2,
toolbox: {
id: "overflow",
name: "Overflow",
modules: [],
spaces: Array.from({ length: 6 }, (_, index) => ({ id: `space-${index}`, name: `Space ${index}`, moduleOrder: { one: [], two: [[], []] } }))
},
modules: {}
}) });
assert.equal(result, null);
assert.equal(writes, 0);
assert.match(errorMessage, /plus de 5 espaces/);
});
test("failed toolbox deletion preserves registry links and modules together", async () => {
const initial = {
toolboxes: [{ id: "existing", modules: [{ id: "note", type: "notepad" }] }],

View file

@ -19,7 +19,12 @@ test("vite entrypoint and app shell are wired", async () => {
assert.match(html, /<div id="app"><\/div>/);
assert.match(html, /\/favicon\.ico/);
assert.match(html, /\/src\/main\.jsx/);
assert.match(html, /_asset-recovery/);
assert.match(html, /vite:preloadError/);
assert.match(html, /HTMLScriptElement/);
assert.match(source, /styles\/main\.scss/);
assert.match(source, /searchParams\.has\("_asset-recovery"\)/);
assert.match(source, /searchParams\.delete\("_asset-recovery"\)/);
assert.match(source, /hooks\/useGameFilters\.js/);
assert.doesNotMatch(source, /DEFAULT_SITE_CONTENT/);
assert.match(gameFiltersHook, /export function useGameFilters/);

View file

@ -91,6 +91,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
const moduleComponents = await readFile("website/src/features/toolboxes/modules/toolboxModuleComponents.jsx", "utf8");
const addToolControls = await readFile("website/src/features/toolboxes/modules/AddToolControls.jsx", "utf8");
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}`;
@ -222,7 +223,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(moduleShell, /module-edit-button/);
assert.match(moduleShell, /itemReorder\.startDrag/);
assert.match(moduleRegistryBundle, /onPointerDown/);
assert.match(toolboxModules, /useGroupedReorder/);
assert.match(toolboxPages, /useGroupedReorder/);
assert.match(toolboxModules, /reorder=\{reorder\}/);
assert.match(moduleShell, /useInlineEdit/);
assert.match(moduleShell, /getContentEditableProps/);
assert.match(addToolControls, /tool-add-card/);

View file

@ -15,6 +15,7 @@ import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, g
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 { 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";
function createTextImportContext() {
@ -989,6 +990,48 @@ test("toolbox modules preserve only active shell display options", () => {
assert.equal("collapsed" in toolbox.modules[1], false);
});
test("legacy toolbox order becomes a persistent default space", () => {
const toolbox = normalizeToolbox({
id: "legacy",
modules: [{ id: "a", type: "notepad" }, { id: "b", type: "links" }],
moduleOrder: { one: ["b", "a"], two: [["a"], ["b"]] }
});
assert.equal(toolbox.spaces.length, 1);
assert.equal(toolbox.spaces[0].name, "Défaut");
assert.equal(toolbox.defaultSpaceId, toolbox.spaces[0].id);
assert.equal(toolbox.activeSpaceId, toolbox.spaces[0].id);
assert.deepEqual(toolbox.spaces[0].moduleOrder, { one: ["b", "a"], two: [["a"], ["b"]] });
});
test("spaces move modules, preserve inactive orders and merge deleted spaces into default", () => {
let toolbox = normalizeToolbox({
id: "spaces",
modules: ["a", "b", "c"].map((id) => ({ id, type: "counters" })),
moduleOrder: { one: ["a", "b", "c"], two: [["a", "b"], ["c"]] }
});
toolbox = createToolboxSpace(toolbox, "Boss");
const bossId = toolbox.activeSpaceId;
toolbox = moveToolboxModule(toolbox, "b", { targetSpaceId: bossId, moduleColumns: 1 });
assert.deepEqual(toolbox.spaces[0].moduleOrder, { one: ["a", "c"], two: [["a"], ["c"]] });
assert.deepEqual(toolbox.spaces[1].moduleOrder, { one: ["b"], two: [["b"], []] });
toolbox = deleteToolboxSpace(toolbox, bossId);
assert.deepEqual(toolbox.spaces[0].moduleOrder, { one: ["a", "c", "b"], two: [["a", "b"], ["c"]] });
});
test("the default space stays first when secondary spaces are reordered", () => {
let toolbox = normalizeToolbox({ modules: [] });
toolbox = createToolboxSpace(toolbox, "Deux");
const secondId = toolbox.activeSpaceId;
toolbox = createToolboxSpace(toolbox, "Trois");
const thirdId = toolbox.activeSpaceId;
toolbox = reorderToolboxSpaces(toolbox, thirdId, secondId, "before");
assert.equal(toolbox.spaces[0].id, toolbox.defaultSpaceId);
assert.deepEqual(toolbox.spaces.slice(1).map((space) => space.id), [thirdId, secondId]);
toolbox = createToolboxSpace(createToolboxSpace(toolbox, "Quatre"), "Cinq");
assert.equal(toolbox.spaces.length, 5);
assert.equal(createToolboxSpace(toolbox, "Six").spaces.length, 5);
});
test("toolbox export remaps module order ids", () => {
const payload = createToolboxExportPayload(
{
@ -1004,11 +1047,32 @@ test("toolbox export remaps module order ids", () => {
);
assert.deepEqual(payload.toolbox.modules.map((module) => module.id), ["m1", "m2", "m3"]);
assert.deepEqual(payload.toolbox.moduleOrder, {
assert.equal(payload.version, 2);
assert.deepEqual(payload.toolbox.spaces[0].moduleOrder, {
one: ["m3", "m1", "m2"],
two: [["m2"], ["m3", "m1"]]
});
assert.deepEqual(normalizeToolbox(payload.toolbox).moduleOrder, payload.toolbox.moduleOrder);
assert.deepEqual(normalizeToolbox(payload.toolbox).spaces[0].moduleOrder, payload.toolbox.spaces[0].moduleOrder);
});
test("toolbox export remaps space ids and preserves the active space", () => {
const payload = createToolboxExportPayload({
id: "toolbox-spaces",
name: "Spaces",
modules: [{ id: "module1", type: "notepad" }, { id: "module2", type: "links" }],
spaces: [
{ id: "default", name: "Défaut", moduleOrder: { one: ["module1"], two: [["module1"], []] } },
{ id: "boss", name: "Boss", moduleOrder: { one: ["module2"], two: [[], ["module2"]] } }
],
defaultSpaceId: "default",
activeSpaceId: "boss"
}, {});
assert.equal(payload.toolbox.spaces.length, 2);
assert.notEqual(payload.toolbox.defaultSpaceId, "default");
assert.notEqual(payload.toolbox.activeSpaceId, "boss");
assert.equal(payload.toolbox.defaultSpaceId, payload.toolbox.spaces[0].id);
assert.equal(payload.toolbox.activeSpaceId, payload.toolbox.spaces[1].id);
assert.deepEqual(payload.toolbox.spaces.map((space) => space.moduleOrder.one), [["m1"], ["m2"]]);
});
test("toolbox export remaps randomizer multi-target exclusions", () => {