Fix toolbox persistence, table evaluation, and onboarding panel styling
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
0171c9fc98
commit
184314d913
9 changed files with 119 additions and 32 deletions
|
|
@ -47,10 +47,11 @@ export function DrawingOverlay({ active, erase, strokes, color, width, onChange,
|
|||
useEffect(() => {
|
||||
function syncSize() {
|
||||
const nextSize = getSurfaceSize?.() || { width: 0, height: 0 };
|
||||
setSize({
|
||||
width: Math.max(0, Math.ceil(nextSize.width || 0)),
|
||||
height: Math.max(0, Math.ceil(nextSize.height || 0))
|
||||
});
|
||||
const nextWidth = Math.max(0, Math.ceil(nextSize.width || 0));
|
||||
const nextHeight = Math.max(0, Math.ceil(nextSize.height || 0));
|
||||
setSize((current) => current.width === nextWidth && current.height === nextHeight
|
||||
? current
|
||||
: { width: nextWidth, height: nextHeight });
|
||||
}
|
||||
|
||||
syncSize();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { DrawingControls } from "./DrawingControls.jsx";
|
|||
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
||||
import { NotepadToolbar } from "./NotepadToolbar.jsx";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
import { normalizeNotepadData } from "../storage/toolboxStorage.js";
|
||||
import {
|
||||
DRAWING_WIDTHS,
|
||||
HIGHLIGHT_COLORS,
|
||||
|
|
@ -21,7 +22,7 @@ import {
|
|||
|
||||
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
|
||||
const normalizedData = useMemo(() => normalizeNotepadData(storedData), [storedData]);
|
||||
const textContent = context.moduleText?.notepad || {};
|
||||
const notify = context.notify;
|
||||
const activePage = normalizedData.pages.find((page) => page.id === normalizedData.activePageId) || normalizedData.pages[0];
|
||||
|
|
@ -43,7 +44,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
const [textColor, setTextColor] = useState(TEXT_COLORS[0]);
|
||||
const [highlightColor, setHighlightColor] = useState(HIGHLIGHT_COLORS[0]);
|
||||
const [editingPageId, setEditingPageId] = useState("");
|
||||
const [pageTitleDrafts, setPageTitleDrafts] = useState({});
|
||||
const [pageTitleDrafts, setPageTitleDrafts] = useState(() => Object.fromEntries(normalizedData.pages.map((page) => [page.id, page.title])));
|
||||
const [activeFormats, setActiveFormats] = useState({
|
||||
bold: false,
|
||||
italic: false,
|
||||
|
|
@ -56,10 +57,6 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
const [temporaryStrokes, setTemporaryStrokes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setPageTitleDrafts(Object.fromEntries(normalizedData.pages.map((page) => [page.id, page.title])));
|
||||
}, [normalizedData.pages]);
|
||||
|
||||
useEffect(() => {
|
||||
const pageKey = `${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
if (pageKeyRef.current !== pageKey) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
import { useTableInteractions } from "./useTableInteractions.js";
|
||||
|
||||
export function TableModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const data = context.normalizeTableData(context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} }));
|
||||
const rawData = context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} });
|
||||
const data = useMemo(() => context.normalizeTableData(rawData), [context, rawData]);
|
||||
const textContent = context.moduleText?.table || {};
|
||||
const evaluatedCells = useMemo(() => evaluateTable(data.cells, data), [data]);
|
||||
const columnWidths = useMemo(() => getColumnWidths(data, evaluatedCells), [data, evaluatedCells]);
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ export function evaluateFormula(expression, resolveReference) {
|
|||
}
|
||||
|
||||
export function evaluateTableCell(address, cells, dimensions, stack = []) {
|
||||
return evaluateTableCellWithCache(address, cells, dimensions, stack, new Map());
|
||||
}
|
||||
|
||||
function evaluateTableCellWithCache(address, cells, dimensions, stack, cache) {
|
||||
if (!stack.length && cache.has(address)) return cache.get(address);
|
||||
const raw = String(cells?.[address] || "").trim();
|
||||
if (!raw) return { value: null, display: "", error: "empty" };
|
||||
|
||||
|
|
@ -176,25 +181,30 @@ export function evaluateTableCell(address, cells, dimensions, stack = []) {
|
|||
|
||||
const result = evaluateFormula(raw.slice(1), (reference) => {
|
||||
if (!CELL_REFERENCE_RE.test(reference) || !isCellInBounds(reference, dimensions.rows, dimensions.columns)) throw new Error("reference");
|
||||
const value = evaluateTableCell(reference, cells, dimensions, [...stack, address]);
|
||||
const value = evaluateTableCellWithCache(reference, cells, dimensions, [...stack, address], cache);
|
||||
if (value.error || value.value == null) throw new Error(value.error === "cycle" ? "cycle" : "reference");
|
||||
return value.value;
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
const label = result.error === "cycle" ? "#CYCLE" : result.error === "division" ? "#DIV/0" : "#ERREUR";
|
||||
return { value: null, display: label, error: result.error };
|
||||
const evaluated = { value: null, display: label, error: result.error };
|
||||
if (!stack.length) cache.set(address, evaluated);
|
||||
return evaluated;
|
||||
}
|
||||
|
||||
return { value: result.value, display: formatNumber(result.value), error: "" };
|
||||
const evaluated = { value: result.value, display: formatNumber(result.value), error: "" };
|
||||
if (!stack.length) cache.set(address, evaluated);
|
||||
return evaluated;
|
||||
}
|
||||
|
||||
export function evaluateTable(cells, dimensions) {
|
||||
const evaluated = {};
|
||||
const cache = new Map();
|
||||
for (let rowIndex = 0; rowIndex < dimensions.rows; rowIndex += 1) {
|
||||
for (let columnIndex = 0; columnIndex < dimensions.columns; columnIndex += 1) {
|
||||
const address = cellAddress(rowIndex, columnIndex);
|
||||
evaluated[address] = evaluateTableCell(address, cells, dimensions);
|
||||
evaluated[address] = evaluateTableCellWithCache(address, cells, dimensions, [], cache);
|
||||
}
|
||||
}
|
||||
return evaluated;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import {
|
|||
setLinks as dbSetLinks,
|
||||
setModuleData as dbSetModuleData,
|
||||
setSetting as dbSetSetting,
|
||||
setToolboxes as dbSetToolboxes
|
||||
setToolboxes as dbSetToolboxes,
|
||||
setToolboxSnapshot as dbSetToolboxSnapshot
|
||||
} from "../../../utils/indexedDbStorage.js";
|
||||
import {
|
||||
compactToolboxesForStorage,
|
||||
|
|
@ -38,6 +39,8 @@ export function useIndexedToolboxes(onError) {
|
|||
const toolboxesRef = useRef(toolboxes);
|
||||
const linksRef = useRef(links);
|
||||
const moduleDataRef = useRef(moduleData);
|
||||
const confirmedModuleDataRef = useRef(moduleData);
|
||||
const moduleWriteQueuesRef = useRef({});
|
||||
const toastPositionRef = useRef(toastPosition);
|
||||
const writeVersionsRef = useRef({
|
||||
toolboxes: 0,
|
||||
|
|
@ -94,7 +97,9 @@ export function useIndexedToolboxes(onError) {
|
|||
if (cancelled) return;
|
||||
commitToolboxes((Array.isArray(storedToolboxes) ? storedToolboxes : []).map(normalizeToolbox).filter(Boolean));
|
||||
commitLinks(storedLinks && typeof storedLinks === "object" ? storedLinks : {});
|
||||
commitModuleData(Object.fromEntries((storedModules || []).map((entry) => [entry.key, entry.data])));
|
||||
const loadedModuleData = Object.fromEntries((storedModules || []).map((entry) => [entry.key, entry.data]));
|
||||
confirmedModuleDataRef.current = loadedModuleData;
|
||||
commitModuleData(loadedModuleData);
|
||||
commitToastPosition(storedToastPosition === "left" ? "left" : "right");
|
||||
setReady(true);
|
||||
refreshQuota();
|
||||
|
|
@ -148,11 +153,22 @@ export function useIndexedToolboxes(onError) {
|
|||
writeVersionsRef.current.moduleDataByKey = commitModuleDataCheckpoints(writeVersionsRef.current.moduleDataByKey, [checkpoint]);
|
||||
const nextModuleData = applyModuleDataValue(moduleDataRef.current, key, compact);
|
||||
commitModuleData(nextModuleData);
|
||||
const operation = compact == null ? dbRemoveModuleData(key) : dbSetModuleData(key, compact);
|
||||
operation.then(refreshQuota).catch((error) => {
|
||||
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, [checkpoint]));
|
||||
const previousQueue = moduleWriteQueuesRef.current[key] || Promise.resolve();
|
||||
const operation = previousQueue.catch(() => {}).then(async () => {
|
||||
if (compact == null) await dbRemoveModuleData(key);
|
||||
else await dbSetModuleData(key, compact);
|
||||
confirmedModuleDataRef.current = applyModuleDataValue(confirmedModuleDataRef.current, key, compact);
|
||||
await refreshQuota();
|
||||
}).catch((error) => {
|
||||
if (moduleDataRef.current[key] === compact) {
|
||||
commitModuleData(applyModuleDataValue(moduleDataRef.current, key, confirmedModuleDataRef.current[key]));
|
||||
}
|
||||
reportStorageError(error);
|
||||
});
|
||||
const queuedOperation = operation.finally(() => {
|
||||
if (moduleWriteQueuesRef.current[key] === queuedOperation) delete moduleWriteQueuesRef.current[key];
|
||||
});
|
||||
moduleWriteQueuesRef.current[key] = queuedOperation;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +195,30 @@ export function useIndexedToolboxes(onError) {
|
|||
});
|
||||
}
|
||||
|
||||
async function persistToolboxSnapshot(nextToolboxes, nextLinks, nextModuleData) {
|
||||
const normalizedToolboxes = compactToolboxesForStorage(nextToolboxes).map(normalizeToolbox).filter(Boolean);
|
||||
const previous = {
|
||||
toolboxes: toolboxesRef.current,
|
||||
links: linksRef.current,
|
||||
moduleData: moduleDataRef.current
|
||||
};
|
||||
commitToolboxes(normalizedToolboxes);
|
||||
commitLinks(nextLinks);
|
||||
commitModuleData(nextModuleData);
|
||||
try {
|
||||
await dbSetToolboxSnapshot(compactToolboxesForStorage(normalizedToolboxes), nextLinks, nextModuleData);
|
||||
confirmedModuleDataRef.current = nextModuleData;
|
||||
await refreshQuota();
|
||||
return true;
|
||||
} catch (error) {
|
||||
commitToolboxes(previous.toolboxes);
|
||||
commitLinks(previous.links);
|
||||
commitModuleData(previous.moduleData);
|
||||
reportStorageError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setToastPosition(position) {
|
||||
const normalized = position === "left" ? "left" : "right";
|
||||
const previousToastPosition = toastPositionRef.current;
|
||||
|
|
@ -206,6 +246,7 @@ export function useIndexedToolboxes(onError) {
|
|||
removeModuleData,
|
||||
removeToolboxModuleData,
|
||||
setToastPosition,
|
||||
refreshQuota
|
||||
refreshQuota,
|
||||
persistToolboxSnapshot
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,13 +61,14 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
|
||||
function deleteToolbox(id) {
|
||||
const toolbox = store.toolboxes.find((item) => item.id === id);
|
||||
store.removeToolboxModuleData(toolbox);
|
||||
store.setToolboxes(store.toolboxes.filter((item) => item.id !== id));
|
||||
const nextToolboxes = store.toolboxes.filter((item) => item.id !== id);
|
||||
const nextModuleData = { ...store.moduleData };
|
||||
(toolbox?.modules || []).forEach((module) => delete nextModuleData[`${id}:${module.id}`]);
|
||||
const nextLinks = { ...store.links };
|
||||
Object.entries(nextLinks).forEach(([gameId, toolboxId]) => {
|
||||
if (toolboxId === id) delete nextLinks[gameId];
|
||||
});
|
||||
store.setLinks(nextLinks);
|
||||
return store.persistToolboxSnapshot(nextToolboxes, nextLinks, nextModuleData);
|
||||
}
|
||||
|
||||
function linkToolboxToGame(gameId, toolboxId) {
|
||||
|
|
@ -146,13 +147,19 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
});
|
||||
imported.moduleOrder = remapModuleOrderIds(imported.moduleOrder, moduleIdMap);
|
||||
const normalizedImport = normalizeToolbox(imported);
|
||||
if (!store.setToolboxes([normalizedImport, ...store.toolboxes])) return null;
|
||||
const nextModuleData = { ...store.moduleData };
|
||||
Object.entries(payload.modules || {}).forEach(([oldId, data]) => {
|
||||
const nextId = moduleIdMap.get(oldId);
|
||||
const module = normalizedImport.modules.find((item) => item.id === nextId);
|
||||
if (nextId) store.updateModuleData(normalizedImport.id, nextId, data, module?.type);
|
||||
if (nextId) {
|
||||
const compact = compactModuleDataForStorage(module?.type, data);
|
||||
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
|
||||
}
|
||||
});
|
||||
if (gameId) linkToolboxToGame(gameId, normalizedImport.id);
|
||||
const nextLinks = { ...store.links };
|
||||
if (gameId) nextLinks[gameId] = normalizedImport.id;
|
||||
const saved = await store.persistToolboxSnapshot([normalizedImport, ...store.toolboxes], nextLinks, nextModuleData);
|
||||
if (!saved) return null;
|
||||
return normalizedImport;
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +188,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
|
||||
if (nextToolboxId) nextLinks[gameId] = nextToolboxId;
|
||||
});
|
||||
const nextModuleData = { ...store.moduleData };
|
||||
Object.entries(payload.modules).forEach(([key, data]) => {
|
||||
const [oldToolboxId] = key.split(":");
|
||||
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
|
||||
|
|
@ -188,11 +196,10 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
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) store.updateModuleData(nextToolboxId, nextModuleId, compact, module?.type);
|
||||
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
|
||||
});
|
||||
store.setToolboxes([...importedToolboxes, ...store.toolboxes]);
|
||||
store.setLinks(nextLinks);
|
||||
store.refreshQuota();
|
||||
const saved = await store.persistToolboxSnapshot([...importedToolboxes, ...store.toolboxes], nextLinks, nextModuleData);
|
||||
if (!saved) throw new Error("Impossible d'enregistrer l'import global.");
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function HomeOnboardingSection({ content, className = "", onCreateToolbox
|
|||
const currentSlide = slides[activeSlide] || slides[0];
|
||||
|
||||
return (
|
||||
<section className={["home-onboarding nebula-panel", className].filter(Boolean).join(" ")} aria-labelledby="home-onboarding-title">
|
||||
<section className={["home-onboarding nds-panel", className].filter(Boolean).join(" ")} aria-labelledby="home-onboarding-title">
|
||||
<div className="home-section-heading home-onboarding-heading">
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h2 id="home-onboarding-title">{content.title}</h2>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,22 @@ export async function removeModuleDataKeys(keys) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function setToolboxSnapshot(toolboxes, links, moduleData) {
|
||||
const db = await openDatabase();
|
||||
await new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([KV_STORE, MODULE_STORE], "readwrite");
|
||||
const kv = transaction.objectStore(KV_STORE);
|
||||
const modules = transaction.objectStore(MODULE_STORE);
|
||||
kv.put(toolboxes, "toolboxes");
|
||||
kv.put(links, "links");
|
||||
modules.clear();
|
||||
Object.entries(moduleData || {}).forEach(([key, data]) => modules.put({ key, data }));
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.onabort = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getStorageEstimate() {
|
||||
if (!navigator.storage?.estimate) return { usage: 0, quota: 0 };
|
||||
return navigator.storage.estimate();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue