sokko-g/website/src/features/toolboxes/useToolboxActions.js
Shinuwa a4cf633ab4
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Serialize toolbox writes and preserve concurrent edits
2026-09-10 19:09:48 +02:00

286 lines
12 KiB
JavaScript

// Rôle : regroupe les actions métier de création, import/export, lien et édition toolbox.
import {
compactModuleDataForStorage,
createGlobalExportPayload,
createToolboxExportPayload,
globalModuleKey,
labelFromFileName,
normalizeToolbox,
remapModuleOrderIds,
uid
} from "./storage/toolboxStorage.js";
import { compressImage } from "../../utils/imageCompression.js";
function downloadJson(payload, filename) {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
function getLibraryUrl() {
if (typeof window === "undefined") return "https://sokko-g.local/#/library";
return `${window.location.origin}${window.location.pathname}#/library`;
}
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
function createToolbox(name, gameId = "", options = {}) {
const now = new Date().toISOString();
const memosModuleId = uid("mod");
const linksModuleId = uid("mod");
const welcomeText = creationTemplate?.memos?.text || creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox.";
const libraryLink = {
id: uid("link"),
title: creationTemplate?.links?.title || "Librairie des outils",
url: getLibraryUrl()
};
const toolbox = {
id: uid("tbx"),
name: name.trim() || "Nouvelle toolbox",
updatedAt: now,
modules: [
{ id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Mémos" },
{ id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" }
]
};
if (options.tutorial === true) toolbox.tutorial = true;
const memos = compactModuleDataForStorage("memos", { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] });
const links = compactModuleDataForStorage("links", { links: [libraryLink] });
void store.persistToolboxSnapshot((current) => ({
toolboxes: [toolbox, ...current.toolboxes],
links: gameId ? { ...current.links, [gameId]: toolbox.id } : current.links,
moduleData: {
...current.moduleData,
[globalModuleKey(toolbox.id, memosModuleId)]: memos,
[globalModuleKey(toolbox.id, linksModuleId)]: links
}
}));
return toolbox;
}
function updateToolbox(nextToolbox) {
const updatedAt = new Date().toISOString();
store.setToolboxes((toolboxes) => toolboxes.map((toolbox) => toolbox.id === nextToolbox.id
? { ...nextToolbox, updatedAt }
: toolbox));
}
function deleteToolbox(id) {
return store.persistToolboxSnapshot((current) => {
const toolbox = current.toolboxes.find((item) => item.id === id);
const moduleData = { ...current.moduleData };
(toolbox?.modules || []).forEach((module) => delete moduleData[`${id}:${module.id}`]);
return {
toolboxes: current.toolboxes.filter((item) => item.id !== id),
moduleData,
links: Object.fromEntries(Object.entries(current.links).filter(([, toolboxId]) => toolboxId !== id))
};
});
}
function linkToolboxToGame(gameId, toolboxId) {
const updatedAt = new Date().toISOString();
return store.persistToolboxSnapshot((current) => {
const links = { ...current.links };
const touched = new Set([links[gameId], toolboxId].filter(Boolean));
if (toolboxId) links[gameId] = toolboxId;
else delete links[gameId];
return {
...current,
links,
toolboxes: current.toolboxes.map((toolbox) => touched.has(toolbox.id) ? { ...toolbox, updatedAt } : toolbox)
};
});
}
async function addImageFiles(toolboxId, moduleId, files) {
const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/"));
if (!imageFiles.length) return false;
const images = [];
for (const file of imageFiles) {
const image = { id: uid("image"), dataUrl: await compressImage(file) };
const label = labelFromFileName(file.name);
if (label) image.label = label;
images.unshift(image);
}
const data = store.getModuleData(toolboxId, moduleId, { images: [] });
return store.updateModuleData(toolboxId, moduleId, {
...data,
images: [...images, ...(Array.isArray(data.images) ? data.images : [])]
});
}
function createChecklistFromList(gameId, list) {
const toolboxId = store.links[gameId] || "";
const toolbox = store.toolboxes.find((item) => item.id === toolboxId);
if (!toolbox || !list) return false;
const moduleId = uid("mod");
const sections = (list.categories || []).map((category) => ({
id: uid("section"),
title: category.title || "",
items: (category.items || []).map((item) => ({
id: uid("item"),
label: item.name,
qtyTarget: Math.max(1, Number.parseInt(item.quantity, 10) || 1),
qtyCurrent: 0
})).filter((item) => item.label)
})).filter((section) => section.items.length);
if (!sections.length) return false;
setConfirmModal({
title: "Créer une checklist",
message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`,
confirmLabel: "Créer",
onResolve: (confirmed) => {
if (!confirmed) return;
const updatedAt = new Date().toISOString();
store.setToolboxes((toolboxes) => toolboxes.map((item) => item.id === toolboxId
? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt }
: item));
store.updateModuleData(toolboxId, moduleId, { sections }, "checklist");
notify("Checklist créée dans la toolbox associée.");
}
});
return true;
}
async function importToolboxPayload(file, gameId = "") {
const payload = JSON.parse(await file.text());
if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide");
const imported = normalizeToolbox({ ...payload.toolbox, id: uid("tbx"), updatedAt: new Date().toISOString() });
const moduleIdMap = new Map();
imported.modules = imported.modules.map((module) => {
const nextId = uid("mod");
moduleIdMap.set(module.id, nextId);
return { ...module, id: nextId };
});
imported.moduleOrder = remapModuleOrderIds(imported.moduleOrder, moduleIdMap);
const normalizedImport = normalizeToolbox(imported);
const nextModuleData = {};
Object.entries(payload.modules || {}).forEach(([oldId, data]) => {
const nextId = moduleIdMap.get(oldId);
const module = normalizedImport.modules.find((item) => item.id === nextId);
if (nextId) {
const compact = compactModuleDataForStorage(module?.type, data);
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
}
});
const saved = await store.persistToolboxSnapshot((current) => ({
toolboxes: [normalizedImport, ...current.toolboxes],
links: gameId ? { ...current.links, [gameId]: normalizedImport.id } : current.links,
moduleData: { ...current.moduleData, ...nextModuleData }
}));
if (!saved) return null;
return normalizedImport;
}
async function importAllToolboxesPayload(file) {
const payload = JSON.parse(await file.text());
if (!Array.isArray(payload.toolboxes) || !payload.modules || typeof payload.modules !== "object") {
throw new Error("Format d'import global invalide");
}
const toolboxIdMap = new Map();
const moduleIdMap = new Map();
const importedToolboxes = payload.toolboxes.map((toolbox) => {
const nextToolboxId = uid("tbx");
toolboxIdMap.set(toolbox.id, nextToolboxId);
const localModuleIdMap = new Map();
const modules = (toolbox.modules || []).map((module) => {
const nextModuleId = uid("mod");
moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId);
localModuleIdMap.set(module.id, nextModuleId);
return { ...module, id: nextModuleId };
});
const moduleOrder = remapModuleOrderIds(toolbox.moduleOrder, localModuleIdMap);
return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, moduleOrder, updatedAt: new Date().toISOString() });
});
const nextLinks = {};
Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => {
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
if (nextToolboxId) nextLinks[gameId] = nextToolboxId;
});
const nextModuleData = {};
Object.entries(payload.modules).forEach(([key, data]) => {
const [oldToolboxId] = key.split(":");
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
const nextModuleId = moduleIdMap.get(key);
const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId);
const module = toolbox?.modules.find((item) => item.id === nextModuleId);
const compact = compactModuleDataForStorage(module?.type, data);
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
});
const saved = await store.persistToolboxSnapshot((current) => ({
toolboxes: [...importedToolboxes, ...current.toolboxes],
links: { ...current.links, ...nextLinks },
moduleData: { ...current.moduleData, ...nextModuleData }
}));
if (!saved) throw new Error("Impossible d'enregistrer l'import global.");
}
return {
createToolbox,
updateToolbox,
deleteToolbox,
linkToolboxToGame,
setCreateModal,
setConfirmModal,
setLinkModalGameId,
setDrawerGameId,
setImage,
createChecklistFromList,
addImageFiles,
notify,
removeModuleData: store.removeModuleData,
updateModuleData: store.updateModuleData,
updateToolboxOrder: (orderedIds) => {
const order = new Map(orderedIds.map((id, index) => [id, index]));
store.setToolboxes((toolboxes) => [...toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999)));
},
importToolbox: async (file, gameId = "") => {
try {
const toolbox = await importToolboxPayload(file, gameId);
if (toolbox) notify("Toolbox importée.");
return toolbox;
} catch (error) {
setConfirmModal({
title: "Import impossible",
message: error.message,
confirmLabel: "Compris",
cancelLabel: "Fermer",
danger: true
});
return null;
}
},
importAllToolboxes: async (file) => {
try {
await importAllToolboxesPayload(file);
notify("Import global terminé.");
return true;
} catch (error) {
setConfirmModal({
title: "Import global impossible",
message: error.message,
confirmLabel: "Compris",
cancelLabel: "Fermer",
danger: true
});
return false;
}
},
exportToolbox: (id) => {
const toolbox = normalizeToolbox(store.toolboxes.find((item) => item.id === id));
if (!toolbox) return;
downloadJson(createToolboxExportPayload(toolbox, store.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`);
notify("Export de la toolbox lancé.");
},
exportAllToolboxes: () => {
downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`);
notify("Export global lancé.");
}
};
}