Add manual toolbox module ordering
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
7140012a98
commit
e043d31fa8
10 changed files with 321 additions and 128 deletions
|
|
@ -16,6 +16,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Vérifier l'affichage dans la page toolbox complète.
|
- Vérifier l'affichage dans la page toolbox complète.
|
||||||
- Vérifier l'affichage dans le panneau latéral.
|
- Vérifier l'affichage dans le panneau latéral.
|
||||||
- Vérifier le mode une colonne et deux colonnes.
|
- Vérifier le mode une colonne et deux colonnes.
|
||||||
|
- Vérifier que l'ajout, la suppression et la réorganisation respectent `moduleOrder.one` et `moduleOrder.two`.
|
||||||
- Vérifier le quota de stockage si l'outil manipule des données lourdes.
|
- Vérifier le quota de stockage si l'outil manipule des données lourdes.
|
||||||
|
|
||||||
## Page Jeu
|
## Page Jeu
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ Les toolboxes sont stockées dans `kv/toolboxes`.
|
||||||
"title": "Armures"
|
"title": "Armures"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layout": "two",
|
"moduleColumns": 2,
|
||||||
|
"moduleOrder": {
|
||||||
|
"one": ["mod1"],
|
||||||
|
"two": [["mod1"], []]
|
||||||
|
},
|
||||||
"updatedAt": "2026-07-25T12:00:00.000Z"
|
"updatedAt": "2026-07-25T12:00:00.000Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -59,7 +63,11 @@ Notes :
|
||||||
|
|
||||||
- `gameId` est vide pour une toolbox libre.
|
- `gameId` est vide pour une toolbox libre.
|
||||||
- `icon` est utilisé uniquement pour les toolboxes libres.
|
- `icon` est utilisé uniquement pour les toolboxes libres.
|
||||||
- `layout` vaut généralement `one` ou `two`.
|
- `moduleColumns` vaut `1` ou `2` et contrôle l'affichage de la page toolbox complète ; le panneau latéral utilise toujours l'ordre une colonne.
|
||||||
|
- `moduleOrder.one` stocke l'ordre du mode une colonne.
|
||||||
|
- `moduleOrder.two` stocke l'ordre explicite des colonnes gauche et droite en mode deux colonnes.
|
||||||
|
- Chaque identifiant d'outil présent dans `modules` doit apparaître exactement une fois dans `moduleOrder.one` et exactement une fois dans l'une des deux colonnes de `moduleOrder.two`.
|
||||||
|
- Si une ancienne toolbox n'a pas encore `moduleOrder.two`, les outils sont répartis automatiquement depuis l'ordre une colonne avec une moitié en colonne gauche et une moitié en colonne droite.
|
||||||
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
|
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
|
||||||
|
|
||||||
## Liens Jeu / Toolbox
|
## Liens Jeu / Toolbox
|
||||||
|
|
|
||||||
|
|
@ -552,6 +552,15 @@ export function validateLibraryData(payload) {
|
||||||
moduleIds.add(module.id);
|
moduleIds.add(module.id);
|
||||||
assert.equal(typeof payload.modules[module.id], "object", `library.modules.${module.id} must be an object`);
|
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`);
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
|
||||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
||||||
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
|
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import assert from "node:assert/strict";
|
||||||
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
|
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
|
||||||
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||||
|
|
||||||
function createTextImportContext() {
|
function createTextImportContext() {
|
||||||
|
|
@ -470,6 +470,48 @@ test("equipment planner export remaps nested ids and socket links", () => {
|
||||||
assert.equal(exported.equipments[0].socketLinks[0].toSocketItemId, exported.equipments[0].socketItems[1].id);
|
assert.equal(exported.equipments[0].socketLinks[0].toSocketItemId, exported.equipments[0].socketItems[1].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("toolbox module order normalizes one and two column layouts", () => {
|
||||||
|
const modules = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];
|
||||||
|
|
||||||
|
assert.deepEqual(normalizeModuleOrder(null, modules), {
|
||||||
|
one: ["a", "b", "c", "d"],
|
||||||
|
two: [["a", "b"], ["c", "d"]]
|
||||||
|
});
|
||||||
|
assert.deepEqual(normalizeModuleOrder({
|
||||||
|
one: ["c", "x", "c"],
|
||||||
|
two: [["d", "d", "x"], ["b"]]
|
||||||
|
}, modules), {
|
||||||
|
one: ["c", "a", "b", "d"],
|
||||||
|
two: [["d", "a"], ["b", "c"]]
|
||||||
|
});
|
||||||
|
assert.deepEqual(normalizeModuleOrder({
|
||||||
|
one: ["a", "b", "c", "d"],
|
||||||
|
two: [["a", "b", "c", "d"], []]
|
||||||
|
}, modules).two, [["a", "b", "c", "d"], []]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toolbox export remaps module order ids", () => {
|
||||||
|
const payload = createToolboxExportPayload(
|
||||||
|
{
|
||||||
|
id: "toolbox1",
|
||||||
|
name: "Order",
|
||||||
|
modules: [{ id: "module1", type: "notepad" }, { id: "module2", type: "links" }, { id: "module3", type: "counters" }],
|
||||||
|
moduleOrder: {
|
||||||
|
one: ["module3", "module1", "module2"],
|
||||||
|
two: [["module2"], ["module3", "module1"]]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(payload.toolbox.modules.map((module) => module.id), ["m1", "m2", "m3"]);
|
||||||
|
assert.deepEqual(payload.toolbox.moduleOrder, {
|
||||||
|
one: ["m3", "m1", "m2"],
|
||||||
|
two: [["m2"], ["m3", "m1"]]
|
||||||
|
});
|
||||||
|
assert.deepEqual(normalizeToolbox(payload.toolbox).moduleOrder, payload.toolbox.moduleOrder);
|
||||||
|
});
|
||||||
|
|
||||||
test("grouped reorder helpers group flat items and complete group order", () => {
|
test("grouped reorder helpers group flat items and complete group order", () => {
|
||||||
const items = [
|
const items = [
|
||||||
{ id: "a", category: "" },
|
{ id: "a", category: "" },
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@
|
||||||
"type": "equipmentPlanner"
|
"type": "equipmentPlanner"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"moduleOrder": {
|
||||||
|
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "ma", "mb", "mc"],
|
||||||
|
"two": [["m1", "m2", "m3", "m4", "m5", "m6"], ["m7", "m8", "m9", "ma", "mb", "mc"]]
|
||||||
|
},
|
||||||
"updatedAt": "2026-08-09T07:34:31.425Z",
|
"updatedAt": "2026-08-09T07:34:31.425Z",
|
||||||
"moduleColumns": 1
|
"moduleColumns": 1
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
normalizeEquipmentPlannerData,
|
normalizeEquipmentPlannerData,
|
||||||
normalizeImageAnnotationData,
|
normalizeImageAnnotationData,
|
||||||
normalizeLinksData,
|
normalizeLinksData,
|
||||||
|
normalizeModuleOrder,
|
||||||
normalizeNotepadData,
|
normalizeNotepadData,
|
||||||
normalizeTableData,
|
normalizeTableData,
|
||||||
normalizeTaskPlannerData,
|
normalizeTaskPlannerData,
|
||||||
|
|
@ -31,6 +32,31 @@ import {
|
||||||
|
|
||||||
const DRAWER_WIDTH_SETTING = "drawerWidth";
|
const DRAWER_WIDTH_SETTING = "drawerWidth";
|
||||||
|
|
||||||
|
function addModuleToOrder(moduleOrder, modules, moduleId) {
|
||||||
|
const order = normalizeModuleOrder(moduleOrder, modules);
|
||||||
|
const two = [
|
||||||
|
order.two[0].filter((id) => id !== moduleId),
|
||||||
|
order.two[1].filter((id) => id !== moduleId)
|
||||||
|
];
|
||||||
|
const targetColumnIndex = two[0].length <= two[1].length ? 0 : 1;
|
||||||
|
two[targetColumnIndex].push(moduleId);
|
||||||
|
return {
|
||||||
|
one: [...order.one.filter((id) => id !== moduleId), moduleId],
|
||||||
|
two
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeModuleFromOrder(moduleOrder, modules, moduleId) {
|
||||||
|
const order = normalizeModuleOrder(moduleOrder, modules);
|
||||||
|
return {
|
||||||
|
one: order.one.filter((id) => id !== moduleId),
|
||||||
|
two: [
|
||||||
|
order.two[0].filter((id) => id !== moduleId),
|
||||||
|
order.two[1].filter((id) => id !== moduleId)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function copyText(value) {
|
async function copyText(value) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
|
|
@ -144,7 +170,7 @@ export function ToolboxPage(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
||||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||||
const moduleText = siteContent.toolboxes.modules;
|
const moduleText = siteContent.toolboxes.modules;
|
||||||
const moduleContext = {
|
const moduleContext = {
|
||||||
|
|
@ -178,28 +204,27 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
|
|
||||||
function addModule(type) {
|
function addModule(type) {
|
||||||
if (!type) return;
|
if (!type) return;
|
||||||
updateToolbox({ ...toolbox, modules: [...toolbox.modules, { id: uid("mod"), type }] });
|
const module = { id: uid("mod"), type };
|
||||||
|
const modules = [...toolbox.modules, module];
|
||||||
|
updateToolbox({
|
||||||
|
...toolbox,
|
||||||
|
modules,
|
||||||
|
moduleOrder: addModuleToOrder(toolbox.moduleOrder, modules, module.id)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createImageAnnotationModule(dataUrl) {
|
function createImageAnnotationModule(dataUrl) {
|
||||||
if (!dataUrl) return;
|
if (!dataUrl) return;
|
||||||
const moduleId = uid("mod");
|
const moduleId = uid("mod");
|
||||||
updateToolbox({ ...toolbox, modules: [...toolbox.modules, { id: moduleId, type: "imageAnnotation" }] });
|
const modules = [...toolbox.modules, { id: moduleId, type: "imageAnnotation" }];
|
||||||
|
updateToolbox({
|
||||||
|
...toolbox,
|
||||||
|
modules,
|
||||||
|
moduleOrder: addModuleToOrder(toolbox.moduleOrder, modules, moduleId)
|
||||||
|
});
|
||||||
updateModuleData(toolbox.id, moduleId, { image: dataUrl, markers: [] }, "imageAnnotation");
|
updateModuleData(toolbox.id, moduleId, { image: dataUrl, markers: [] }, "imageAnnotation");
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveModule(fromModuleId, toModuleId, placement = "before") {
|
|
||||||
if (!fromModuleId || !toModuleId || fromModuleId === toModuleId) return;
|
|
||||||
const modules = [...toolbox.modules];
|
|
||||||
const fromIndex = modules.findIndex((module) => module.id === fromModuleId);
|
|
||||||
const toIndex = modules.findIndex((module) => module.id === toModuleId);
|
|
||||||
if (fromIndex < 0 || toIndex < 0) return;
|
|
||||||
const [moved] = modules.splice(fromIndex, 1);
|
|
||||||
const targetIndex = modules.findIndex((module) => module.id === toModuleId);
|
|
||||||
modules.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, moved);
|
|
||||||
updateToolbox({ ...toolbox, modules });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={embedded ? "toolbox-embedded" : "toolbox-page"}>
|
<div className={embedded ? "toolbox-embedded" : "toolbox-page"}>
|
||||||
<section className={embedded ? "toolbox-head page-hero toolbox-head-embedded" : "toolbox-head page-hero"}>
|
<section className={embedded ? "toolbox-head page-hero toolbox-head-embedded" : "toolbox-head page-hero"}>
|
||||||
|
|
@ -241,6 +266,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
context={moduleContext}
|
context={moduleContext}
|
||||||
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
||||||
onUpdateModule={(moduleId, updater) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? updater(module) : module) })}
|
onUpdateModule={(moduleId, updater) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? updater(module) : module) })}
|
||||||
|
onModuleOrderChange={(moduleOrder) => updateToolbox({ ...toolbox, moduleOrder })}
|
||||||
onDelete={(moduleId) => actions.setConfirmModal({
|
onDelete={(moduleId) => actions.setConfirmModal({
|
||||||
title: "Retirer l'outil",
|
title: "Retirer l'outil",
|
||||||
message: "Retirer cet outil de la toolbox ?",
|
message: "Retirer cet outil de la toolbox ?",
|
||||||
|
|
@ -249,10 +275,14 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
onResolve: (confirmed) => {
|
onResolve: (confirmed) => {
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
actions.removeModuleData(toolbox.id, moduleId);
|
actions.removeModuleData(toolbox.id, moduleId);
|
||||||
updateToolbox({ ...toolbox, modules: toolbox.modules.filter((module) => module.id !== moduleId) });
|
const modules = toolbox.modules.filter((module) => module.id !== moduleId);
|
||||||
|
updateToolbox({
|
||||||
|
...toolbox,
|
||||||
|
modules,
|
||||||
|
moduleOrder: removeModuleFromOrder(toolbox.moduleOrder, modules, moduleId)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
onMove={moveModule}
|
|
||||||
/>
|
/>
|
||||||
{!embedded && <StorageQuota usage={storageUsage} />}
|
{!embedded && <StorageQuota usage={storageUsage} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : registre des outils toolbox, rendu commun et contrôles d'ajout/réorganisation.
|
// Rôle : registre des outils toolbox, rendu commun et contrôles d'ajout/réorganisation.
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
|
@ -34,51 +34,63 @@ const MODULE_COMPONENTS = {
|
||||||
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_COLUMN_GAP_PX = 16;
|
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||||
|
|
||||||
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
|
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
|
||||||
type,
|
type,
|
||||||
{ label: module.label, icon: module.icon }
|
{ label: module.label, icon: module.icon }
|
||||||
]));
|
]));
|
||||||
|
|
||||||
function getSequentialModuleColumns(modules, splitIndex) {
|
function getModuleMap(modules) {
|
||||||
return [
|
return new Map(modules.map((module) => [module.id, module]));
|
||||||
modules.slice(0, splitIndex),
|
}
|
||||||
modules.slice(splitIndex)
|
|
||||||
|
function getModulesByIds(modules, ids) {
|
||||||
|
const moduleMap = getModuleMap(modules);
|
||||||
|
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModuleColumnId(moduleId, twoColumnOrder) {
|
||||||
|
return twoColumnOrder?.[1]?.includes(moduleId) ? MODULE_COLUMN_IDS[1] : MODULE_COLUMN_IDS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveId(ids, sourceId, targetId, placement = "before") {
|
||||||
|
const nextIds = ids.filter((id) => id !== sourceId);
|
||||||
|
const targetIndex = nextIds.indexOf(targetId);
|
||||||
|
if (targetIndex < 0) return ids;
|
||||||
|
nextIds.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, sourceId);
|
||||||
|
return nextIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveModuleInOneColumnOrder(order, operation) {
|
||||||
|
if (!operation.sourceId || !operation.targetId || operation.sourceId === operation.targetId) return order;
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
one: moveId(order.one, operation.sourceId, operation.targetId, operation.placement)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveModuleInTwoColumnOrder(order, operation) {
|
||||||
|
const columns = [
|
||||||
|
[...(order.two?.[0] || [])],
|
||||||
|
[...(order.two?.[1] || [])]
|
||||||
];
|
];
|
||||||
|
const sourceColumnIndex = columns.findIndex((column) => column.includes(operation.sourceId));
|
||||||
|
if (sourceColumnIndex < 0) return order;
|
||||||
|
const targetColumnIndex = operation.targetType === "item"
|
||||||
|
? columns.findIndex((column) => column.includes(operation.targetId))
|
||||||
|
: MODULE_COLUMN_IDS.indexOf(operation.targetGroup);
|
||||||
|
if (targetColumnIndex < 0) return order;
|
||||||
|
|
||||||
|
const [movedId] = columns[sourceColumnIndex].splice(columns[sourceColumnIndex].indexOf(operation.sourceId), 1);
|
||||||
|
if (operation.targetType === "item") {
|
||||||
|
const targetIndex = columns[targetColumnIndex].indexOf(operation.targetId);
|
||||||
|
if (targetIndex < 0) return order;
|
||||||
|
columns[targetColumnIndex].splice(operation.placement === "after" ? targetIndex + 1 : targetIndex, 0, movedId);
|
||||||
|
} else {
|
||||||
|
columns[targetColumnIndex].splice(operation.placement === "after" ? columns[targetColumnIndex].length : 0, 0, movedId);
|
||||||
}
|
}
|
||||||
|
return { ...order, two: columns };
|
||||||
function getColumnHeight(heights) {
|
|
||||||
if (!heights.length) return 0;
|
|
||||||
return heights.reduce((total, height) => total + height, 0) + MODULE_COLUMN_GAP_PX * (heights.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseMeasuredSplitIndex(modules, moduleHeights, fallbackSplitIndex) {
|
|
||||||
if (modules.length <= 1) return modules.length;
|
|
||||||
if (modules.some((module) => !moduleHeights.get(module.id))) return fallbackSplitIndex;
|
|
||||||
|
|
||||||
let bestSplitIndex = fallbackSplitIndex;
|
|
||||||
let bestDiff = Number.POSITIVE_INFINITY;
|
|
||||||
let bestLeftDominantSplitIndex = 0;
|
|
||||||
let bestLeftDominantDiff = Number.POSITIVE_INFINITY;
|
|
||||||
|
|
||||||
for (let splitIndex = 1; splitIndex < modules.length; splitIndex += 1) {
|
|
||||||
const leftHeight = getColumnHeight(modules.slice(0, splitIndex).map((module) => moduleHeights.get(module.id)));
|
|
||||||
const rightHeight = getColumnHeight(modules.slice(splitIndex).map((module) => moduleHeights.get(module.id)));
|
|
||||||
const diff = Math.abs(leftHeight - rightHeight);
|
|
||||||
|
|
||||||
if (leftHeight >= rightHeight && diff < bestLeftDominantDiff) {
|
|
||||||
bestLeftDominantDiff = diff;
|
|
||||||
bestLeftDominantSplitIndex = splitIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff < bestDiff) {
|
|
||||||
bestDiff = diff;
|
|
||||||
bestSplitIndex = splitIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestLeftDominantSplitIndex || bestSplitIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddToolControls({ onAdd }) {
|
export function AddToolControls({ onAdd }) {
|
||||||
|
|
@ -178,100 +190,74 @@ export function AddToolControls({ onAdd }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUpdateModule, onDelete, onMove }) {
|
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUpdateModule, onDelete, onModuleOrderChange }) {
|
||||||
const [measuredSplitIndex, setMeasuredSplitIndex] = useState(() => Math.ceil(toolbox.modules.length / 2));
|
|
||||||
const moduleElementsRef = useRef(new Map());
|
|
||||||
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
||||||
|
const twoColumnOrder = toolbox.moduleOrder?.two || [[], []];
|
||||||
|
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, toolbox.moduleOrder?.one || []), [toolbox.modules, toolbox.moduleOrder?.one, moduleIdSignature]);
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
getModulesByIds(toolbox.modules, twoColumnOrder[0]),
|
||||||
|
getModulesByIds(toolbox.modules, twoColumnOrder[1])
|
||||||
|
], [toolbox.modules, twoColumnOrder, moduleIdSignature]);
|
||||||
const {
|
const {
|
||||||
itemReorder,
|
itemReorder,
|
||||||
getItemProps,
|
getItemProps,
|
||||||
|
getGroupBoundaryProps,
|
||||||
isItemDragging,
|
isItemDragging,
|
||||||
isItemDropTarget,
|
isItemDropTarget,
|
||||||
|
isBoundaryDropTarget,
|
||||||
getDropPlacement
|
getDropPlacement
|
||||||
} = useGroupedReorder({
|
} = useGroupedReorder({
|
||||||
namespace: "toolbox-modules",
|
namespace: "toolbox-modules",
|
||||||
items: toolbox.modules,
|
items: toolbox.modules,
|
||||||
getItemId: (module) => module.id,
|
getItemId: (module) => module.id,
|
||||||
|
getItemGroup: (module) => moduleColumns === 2 ? getModuleColumnId(module.id, twoColumnOrder) : "",
|
||||||
|
getEffectiveGroup: (module) => moduleColumns === 2 ? getModuleColumnId(module.id, twoColumnOrder) : "",
|
||||||
getParentId: () => toolbox.id,
|
getParentId: () => toolbox.id,
|
||||||
onItemMove: (operation) => onMove(operation.sourceId, operation.targetId, operation.placement),
|
reorderFeatures: {
|
||||||
|
item: {
|
||||||
|
groupChange: moduleColumns === 2,
|
||||||
|
boundaryDrop: moduleColumns === 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemMove: (operation) => {
|
||||||
|
const nextOrder = moduleColumns === 2
|
||||||
|
? moveModuleInTwoColumnOrder(toolbox.moduleOrder, operation)
|
||||||
|
: moveModuleInOneColumnOrder(toolbox.moduleOrder, operation);
|
||||||
|
onModuleOrderChange(nextOrder);
|
||||||
|
},
|
||||||
hierarchy: { enabled: true, stickyParents: true }
|
hierarchy: { enabled: true, stickyParents: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMeasuredSplitIndex(Math.ceil(toolbox.modules.length / 2));
|
|
||||||
}, [toolbox.id, moduleIdSignature, toolbox.modules.length]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (moduleColumns !== 2 || toolbox.modules.length <= 1) return undefined;
|
|
||||||
|
|
||||||
let frameId = 0;
|
|
||||||
function rebalanceColumns() {
|
|
||||||
cancelAnimationFrame(frameId);
|
|
||||||
frameId = requestAnimationFrame(() => {
|
|
||||||
const moduleHeights = new Map();
|
|
||||||
toolbox.modules.forEach((module) => {
|
|
||||||
const element = moduleElementsRef.current.get(module.id);
|
|
||||||
if (element) moduleHeights.set(module.id, element.getBoundingClientRect().height);
|
|
||||||
});
|
|
||||||
|
|
||||||
setMeasuredSplitIndex((currentSplitIndex) => {
|
|
||||||
const fallbackSplitIndex = Math.min(Math.max(currentSplitIndex, 1), toolbox.modules.length - 1);
|
|
||||||
const nextSplitIndex = chooseMeasuredSplitIndex(toolbox.modules, moduleHeights, fallbackSplitIndex);
|
|
||||||
return nextSplitIndex === currentSplitIndex ? currentSplitIndex : nextSplitIndex;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(rebalanceColumns);
|
|
||||||
|
|
||||||
toolbox.modules.forEach((module) => {
|
|
||||||
const element = moduleElementsRef.current.get(module.id);
|
|
||||||
if (element) observer.observe(element);
|
|
||||||
});
|
|
||||||
rebalanceColumns();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(frameId);
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [moduleColumns, toolbox.id, moduleIdSignature, toolbox.modules.length]);
|
|
||||||
|
|
||||||
function registerModuleElement(moduleId, element) {
|
|
||||||
if (element) {
|
|
||||||
moduleElementsRef.current.set(moduleId, element);
|
|
||||||
} else {
|
|
||||||
moduleElementsRef.current.delete(moduleId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleColumns === 1) {
|
if (moduleColumns === 1) {
|
||||||
return (
|
return (
|
||||||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||||
{toolbox.modules.map((module) => (
|
{orderedModules.map((module) => (
|
||||||
<ModuleShell
|
<ModuleShell
|
||||||
key={module.id}
|
key={module.id}
|
||||||
toolbox={toolbox}
|
toolbox={toolbox}
|
||||||
module={module}
|
module={module}
|
||||||
context={context}
|
context={context}
|
||||||
reorder={{ itemReorder, getItemProps, isItemDragging, isItemDropTarget, getDropPlacement }}
|
reorder={{ itemReorder, getItemProps, isItemDragging, isItemDropTarget, getDropPlacement }}
|
||||||
|
itemGroup=""
|
||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
onUpdateModule={onUpdateModule}
|
onUpdateModule={onUpdateModule}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onMove={onMove}
|
|
||||||
registerModuleElement={registerModuleElement}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitIndex = Math.min(Math.max(measuredSplitIndex, 1), Math.max(1, toolbox.modules.length - 1));
|
const hasEmptyColumn = toolbox.modules.length > 1 && columns.some((modules) => modules.length === 0);
|
||||||
const columns = getSequentialModuleColumns(toolbox.modules, splitIndex);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||||
{columns.map((modules, index) => (
|
{columns.map((modules, index) => (
|
||||||
<div className="module-column" key={index}>
|
<div className={`module-column ${hasEmptyColumn && !modules.length ? "is-empty-warning" : ""}`} key={MODULE_COLUMN_IDS[index]}>
|
||||||
|
<div
|
||||||
|
className={`module-column-boundary-drop-zone ${isBoundaryDropTarget({ groupId: MODULE_COLUMN_IDS[index], parentId: toolbox.id, placement: "before" }) ? "is-drop-target" : ""}`}
|
||||||
|
{...getGroupBoundaryProps({ groupId: MODULE_COLUMN_IDS[index], parentId: toolbox.id, placement: "before" })}
|
||||||
|
/>
|
||||||
{modules.map((module) => (
|
{modules.map((module) => (
|
||||||
<ModuleShell
|
<ModuleShell
|
||||||
key={module.id}
|
key={module.id}
|
||||||
|
|
@ -279,20 +265,23 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUp
|
||||||
module={module}
|
module={module}
|
||||||
context={context}
|
context={context}
|
||||||
reorder={{ itemReorder, getItemProps, isItemDragging, isItemDropTarget, getDropPlacement }}
|
reorder={{ itemReorder, getItemProps, isItemDragging, isItemDropTarget, getDropPlacement }}
|
||||||
|
itemGroup={MODULE_COLUMN_IDS[index]}
|
||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
onUpdateModule={onUpdateModule}
|
onUpdateModule={onUpdateModule}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onMove={onMove}
|
|
||||||
registerModuleElement={registerModuleElement}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<div
|
||||||
|
className={`module-column-boundary-drop-zone ${isBoundaryDropTarget({ groupId: MODULE_COLUMN_IDS[index], parentId: toolbox.id, placement: "after" }) ? "is-drop-target" : ""}`}
|
||||||
|
{...getGroupBoundaryProps({ groupId: MODULE_COLUMN_IDS[index], parentId: toolbox.id, placement: "after" })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModuleShell({ toolbox, module, context, reorder, onRename, onUpdateModule, onDelete, registerModuleElement }) {
|
function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRename, onUpdateModule, onDelete }) {
|
||||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||||
const Component = definition.Component;
|
const Component = definition.Component;
|
||||||
const label = definition.label || module.type;
|
const label = definition.label || module.type;
|
||||||
|
|
@ -309,8 +298,7 @@ function ModuleShell({ toolbox, module, context, reorder, onRename, onUpdateModu
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={className}
|
className={className}
|
||||||
{...reorder.getItemProps({ itemId: module.id, parentId: toolbox.id })}
|
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
||||||
ref={(element) => registerModuleElement?.(module.id, element)}
|
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -121,14 +121,70 @@ function normalizeToolboxModule(module) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uniqueKnownIds(ids, knownIds) {
|
||||||
|
const known = new Set(knownIds);
|
||||||
|
const seen = new Set();
|
||||||
|
return (Array.isArray(ids) ? ids : [])
|
||||||
|
.map((id) => String(id || ""))
|
||||||
|
.filter((id) => {
|
||||||
|
if (!known.has(id) || seen.has(id)) return false;
|
||||||
|
seen.add(id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeModuleOrder(ids, knownIds) {
|
||||||
|
const ordered = uniqueKnownIds(ids, knownIds);
|
||||||
|
const used = new Set(ordered);
|
||||||
|
return [...ordered, ...knownIds.filter((id) => !used.has(id))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitModuleOrder(ids) {
|
||||||
|
const splitIndex = Math.ceil(ids.length / 2);
|
||||||
|
return [ids.slice(0, splitIndex), ids.slice(splitIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeModuleOrder(moduleOrder, modules) {
|
||||||
|
const moduleIds = modules.map((module) => module.id);
|
||||||
|
const one = completeModuleOrder(moduleOrder?.one, moduleIds);
|
||||||
|
const hasTwoColumnOrder = Array.isArray(moduleOrder?.two) && Array.isArray(moduleOrder.two[0]) && Array.isArray(moduleOrder.two[1]);
|
||||||
|
if (!hasTwoColumnOrder) return { one, two: splitModuleOrder(one) };
|
||||||
|
|
||||||
|
const firstColumn = uniqueKnownIds(moduleOrder.two[0], moduleIds);
|
||||||
|
const firstColumnIds = new Set(firstColumn);
|
||||||
|
const secondColumn = uniqueKnownIds(moduleOrder.two[1], moduleIds).filter((id) => !firstColumnIds.has(id));
|
||||||
|
const used = new Set([...firstColumn, ...secondColumn]);
|
||||||
|
const missing = moduleIds.filter((id) => !used.has(id));
|
||||||
|
const two = [firstColumn, secondColumn];
|
||||||
|
missing.forEach((id) => {
|
||||||
|
const targetIndex = two[0].length <= two[1].length ? 0 : 1;
|
||||||
|
two[targetIndex].push(id);
|
||||||
|
});
|
||||||
|
return { one, two };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remapModuleOrderIds(moduleOrder, moduleIdMap) {
|
||||||
|
if (!moduleOrder) return null;
|
||||||
|
const remapIds = (ids) => (Array.isArray(ids) ? ids.map((id) => moduleIdMap.get(id)).filter(Boolean) : []);
|
||||||
|
return {
|
||||||
|
one: remapIds(moduleOrder.one),
|
||||||
|
two: [
|
||||||
|
remapIds(moduleOrder.two?.[0]),
|
||||||
|
remapIds(moduleOrder.two?.[1])
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeToolbox(toolbox) {
|
export function normalizeToolbox(toolbox) {
|
||||||
if (!toolbox || typeof toolbox !== "object") return null;
|
if (!toolbox || typeof toolbox !== "object") return null;
|
||||||
|
const modules = (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean);
|
||||||
return {
|
return {
|
||||||
id: toolbox.id || uid("tbx"),
|
id: toolbox.id || uid("tbx"),
|
||||||
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
|
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
|
||||||
icon: normalizeToolboxIcon(toolbox.icon),
|
icon: normalizeToolboxIcon(toolbox.icon),
|
||||||
moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2,
|
moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2,
|
||||||
modules: (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean),
|
modules,
|
||||||
|
moduleOrder: normalizeModuleOrder(toolbox.moduleOrder, modules),
|
||||||
updatedAt: toolbox.updatedAt || new Date().toISOString()
|
updatedAt: toolbox.updatedAt || new Date().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +196,7 @@ export function compactToolboxForStorage(toolbox) {
|
||||||
id: normalized.id,
|
id: normalized.id,
|
||||||
name: normalized.name,
|
name: normalized.name,
|
||||||
modules: normalized.modules,
|
modules: normalized.modules,
|
||||||
|
moduleOrder: normalized.moduleOrder,
|
||||||
updatedAt: normalized.updatedAt
|
updatedAt: normalized.updatedAt
|
||||||
};
|
};
|
||||||
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
|
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
|
||||||
|
|
@ -1413,7 +1470,8 @@ export function createToolboxExportPayload(toolbox, moduleData) {
|
||||||
const id = nextId("mod");
|
const id = nextId("mod");
|
||||||
moduleIdMap.set(module.id, id);
|
moduleIdMap.set(module.id, id);
|
||||||
return { ...module, id };
|
return { ...module, id };
|
||||||
})
|
}),
|
||||||
|
moduleOrder: remapModuleOrderIds(source.moduleOrder, moduleIdMap)
|
||||||
});
|
});
|
||||||
const modules = Object.fromEntries(source.modules
|
const modules = Object.fromEntries(source.modules
|
||||||
.map((module) => [
|
.map((module) => [
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
globalModuleKey,
|
globalModuleKey,
|
||||||
labelFromFileName,
|
labelFromFileName,
|
||||||
normalizeToolbox,
|
normalizeToolbox,
|
||||||
|
remapModuleOrderIds,
|
||||||
uid
|
uid
|
||||||
} from "./storage/toolboxStorage.js";
|
} from "./storage/toolboxStorage.js";
|
||||||
import { compressImage } from "../../utils/imageCompression.js";
|
import { compressImage } from "../../utils/imageCompression.js";
|
||||||
|
|
@ -123,14 +124,16 @@ export function useToolboxActions({ store, notify, setConfirmModal, setCreateMod
|
||||||
moduleIdMap.set(module.id, nextId);
|
moduleIdMap.set(module.id, nextId);
|
||||||
return { ...module, id: nextId };
|
return { ...module, id: nextId };
|
||||||
});
|
});
|
||||||
if (!store.setToolboxes([imported, ...store.toolboxes])) return null;
|
imported.moduleOrder = remapModuleOrderIds(imported.moduleOrder, moduleIdMap);
|
||||||
|
const normalizedImport = normalizeToolbox(imported);
|
||||||
|
if (!store.setToolboxes([normalizedImport, ...store.toolboxes])) return null;
|
||||||
Object.entries(payload.modules || {}).forEach(([oldId, data]) => {
|
Object.entries(payload.modules || {}).forEach(([oldId, data]) => {
|
||||||
const nextId = moduleIdMap.get(oldId);
|
const nextId = moduleIdMap.get(oldId);
|
||||||
const module = imported.modules.find((item) => item.id === nextId);
|
const module = normalizedImport.modules.find((item) => item.id === nextId);
|
||||||
if (nextId) store.updateModuleData(imported.id, nextId, data, module?.type);
|
if (nextId) store.updateModuleData(normalizedImport.id, nextId, data, module?.type);
|
||||||
});
|
});
|
||||||
if (gameId) linkToolboxToGame(gameId, imported.id);
|
if (gameId) linkToolboxToGame(gameId, normalizedImport.id);
|
||||||
return imported;
|
return normalizedImport;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importAllToolboxesPayload(file) {
|
async function importAllToolboxesPayload(file) {
|
||||||
|
|
@ -143,12 +146,15 @@ export function useToolboxActions({ store, notify, setConfirmModal, setCreateMod
|
||||||
const importedToolboxes = payload.toolboxes.map((toolbox) => {
|
const importedToolboxes = payload.toolboxes.map((toolbox) => {
|
||||||
const nextToolboxId = uid("tbx");
|
const nextToolboxId = uid("tbx");
|
||||||
toolboxIdMap.set(toolbox.id, nextToolboxId);
|
toolboxIdMap.set(toolbox.id, nextToolboxId);
|
||||||
|
const localModuleIdMap = new Map();
|
||||||
const modules = (toolbox.modules || []).map((module) => {
|
const modules = (toolbox.modules || []).map((module) => {
|
||||||
const nextModuleId = uid("mod");
|
const nextModuleId = uid("mod");
|
||||||
moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId);
|
moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId);
|
||||||
|
localModuleIdMap.set(module.id, nextModuleId);
|
||||||
return { ...module, id: nextModuleId };
|
return { ...module, id: nextModuleId };
|
||||||
});
|
});
|
||||||
return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, updatedAt: new Date().toISOString() });
|
const moduleOrder = remapModuleOrderIds(toolbox.moduleOrder, localModuleIdMap);
|
||||||
|
return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, moduleOrder, updatedAt: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
const nextLinks = { ...store.links };
|
const nextLinks = { ...store.links };
|
||||||
Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => {
|
Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => {
|
||||||
|
|
|
||||||
|
|
@ -418,8 +418,36 @@
|
||||||
|
|
||||||
.module-column {
|
.module-column {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-3);
|
||||||
align-content: start;
|
align-content: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-column.is-empty-warning {
|
||||||
|
min-height: 132px;
|
||||||
|
border: 1px dashed rgba(246, 196, 83, 0.26);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(246, 196, 83, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-column-boundary-drop-zone {
|
||||||
|
position: relative;
|
||||||
|
min-height: 10px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-column-boundary-drop-zone.is-drop-target::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(246, 196, 83, 0.9);
|
||||||
|
box-shadow: 0 0 12px rgba(246, 196, 83, 0.24);
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
|
|
@ -512,6 +540,25 @@
|
||||||
box-shadow: var(--reorder-drop-shadow, none), inset -3px 0 0 rgba(246, 196, 83, 0.86);
|
box-shadow: var(--reorder-drop-shadow, none), inset -3px 0 0 rgba(246, 196, 83, 0.86);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-sm),
|
||||||
|
inset 0 3px 0 rgba(246, 196, 83, 0.86),
|
||||||
|
0 0 14px rgba(246, 196, 83, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target.drop-after {
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-sm),
|
||||||
|
inset 0 -3px 0 rgba(246, 196, 83, 0.86),
|
||||||
|
0 0 14px rgba(246, 196, 83, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.module header {
|
.module header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue