Isolate RNG drafts and fix image editor lifecycle
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
7c0b72f914
commit
b470a34b2a
7 changed files with 297 additions and 50 deletions
|
|
@ -31,3 +31,4 @@
|
|||
{"id":"int-147c4573ce40577c21afec08775c2811","kind":"field_change","created_at":"2026-09-11T07:16:09.749977471Z","actor":"Shinuwa","issue_id":"sokko-g-55a","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Restored recommended purity lint; initialized alert clock in effect; removed post-commit draft synchronization from shared inline editing and reused it in Randomizer. 100 Node tests, lint, production build and all 19 E2E pass. StrictMode hook tests cover input/contentEditable, source updates, commit and Escape; broader C10 tracked in sokko-g-8po."}}
|
||||
{"id":"int-f9e84a155b896c7f68c83ee2454f904d","kind":"field_change","created_at":"2026-09-11T11:56:43.680278965Z","actor":"Shinuwa","issue_id":"sokko-g-sc7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reproduced memo draft loss on notification, isolated per-memo drafts using useInlineEdit, preserved multiline/trim/delete behavior. Goal Tree reconciles selection and valid tabs before DOM commit with StrictMode regressions. npm run check:full passed: 100 Node tests and 21 E2E."}}
|
||||
{"id":"int-4b64d68baf132fa99cb3244d8487034c","kind":"field_change","created_at":"2026-09-11T12:05:04.580370366Z","actor":"Shinuwa","issue_id":"sokko-g-kqo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Shared keyed state initializes Notepad/ImageAnnotation temporary drawings and RNG history before DOM commit; ignores setters bound to a different active key. Storage keys and write/error paths preserved. npm run check:full passed: 100 Node tests and 23 E2E including StrictMode key switches and real Notepad canvas/page/reload checks."}}
|
||||
{"id":"int-80659dd55d71f2a33d1067b008bf2abd","kind":"field_change","created_at":"2026-09-11T17:30:09.794064421Z","actor":"Shinuwa","issue_id":"sokko-g-275","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed RNG draft/edit transitions and ImageViewer pre-commit resets; cover decoding now guards stale completion/errors and releases resources. Four new StrictMode E2E cases pass; npm run check:full passes with 27 E2E. No storage or style changes."}}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
- Vérifier l'affichage dans le panneau latéral.
|
||||
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
||||
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
||||
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
||||
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
|
||||
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
|
||||
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante.
|
||||
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
||||
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
||||
|
|
|
|||
166
e2e/editor-lifecycle.spec.js
Normal file
166
e2e/editor-lifecycle.spec.js
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Rôle : vérifie les brouillons RNG et les changements d'image sans état périmé sous StrictMode.
|
||||
/* global window, document */
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/editor-lifecycle-probe.js", import.meta.url))}`;
|
||||
|
||||
async function setup(page, kind, props = {}) {
|
||||
await page.goto("/");
|
||||
await page.evaluate(async ({ url, kind, props }) => {
|
||||
const { mountEditorProbe, controlImageDecoding } = await import(url);
|
||||
document.getElementById("app").style.display = "none";
|
||||
const element = document.createElement("div");
|
||||
element.id = "editor-probe";
|
||||
document.body.prepend(element);
|
||||
if (kind === "cover" || kind === "viewer") {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 800;
|
||||
canvas.height = 400;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.fillStyle = "#22d3ee";
|
||||
ctx.fillRect(0, 0, 800, 400);
|
||||
window.probeImage = canvas.toDataURL();
|
||||
if (kind === "cover") {
|
||||
window.probeFile = await new Promise((resolve) => canvas.toBlob(resolve));
|
||||
window.decoding = controlImageDecoding();
|
||||
if (props.existingCover) props.existingCover = window.probeImage;
|
||||
else props.file = window.probeFile;
|
||||
} else {
|
||||
window.viewerImage = {
|
||||
dataUrl: window.probeImage, canAnnotate: true,
|
||||
markers: [{ id: "a", label: "First", x: 30, y: 30 }],
|
||||
onChangeMarkers: () => {}, onChangeDrawings: () => {}
|
||||
};
|
||||
props.image = window.viewerImage;
|
||||
}
|
||||
}
|
||||
window.editorProbe = mountEditorProbe(element, kind, props);
|
||||
}, { url: probeUrl, kind, props });
|
||||
return page.locator("#editor-probe");
|
||||
}
|
||||
|
||||
test("RNG drafts follow rate and module identity without losing unrelated edits", async ({ page }) => {
|
||||
const rates = [
|
||||
{ id: "a", label: "First", rate: "10", costPerDraw: "12" },
|
||||
{ id: "b", label: "Second", rate: "20", costPerDraw: "34", occurrenceValue: 2, occurrenceUnit: "perDay" }
|
||||
];
|
||||
const editor = await setup(page, "rng", { data: { rates, activeRateId: "a" } });
|
||||
const cost = editor.getByLabel("Coût d'un tirage");
|
||||
await cost.fill("999");
|
||||
await page.evaluate(() => window.editorProbe.update({}));
|
||||
await expect(cost).toHaveValue("999");
|
||||
const snapshots = await page.evaluate((rates) => {
|
||||
window.editorProbe.update({ data: { rates, activeRateId: "b" } });
|
||||
return window.editorProbe.snapshots;
|
||||
}, rates);
|
||||
expect(snapshots.length).toBeGreaterThan(0);
|
||||
for (const values of snapshots) {
|
||||
expect(values).toContain("34");
|
||||
expect(values).not.toContain("999");
|
||||
}
|
||||
await editor.getByRole("tab", { name: "Prévisions", exact: true }).click();
|
||||
await expect(editor.getByRole("combobox", { name: "Période", exact: true })).toHaveValue("perDay");
|
||||
await cost.fill("777");
|
||||
await page.evaluate(() => window.editorProbe.update({ moduleId: "other" }));
|
||||
await expect(cost).toHaveValue("34");
|
||||
await cost.fill("56");
|
||||
await cost.press("Tab");
|
||||
expect(await page.evaluate(() => window.editorProbe.saves.at(-1).rates[1].costPerDraw)).toBe("56");
|
||||
await page.evaluate(() => window.editorProbe.update({ editing: true }));
|
||||
const label = editor.locator('.rng-simulator-rate-form input').first();
|
||||
await label.fill("Unsubmitted");
|
||||
await page.evaluate(() => window.editorProbe.update({ editing: false }));
|
||||
await expect(label).toHaveCount(0);
|
||||
await page.evaluate(() => window.editorProbe.update({ editing: true }));
|
||||
await expect(label).toHaveValue("");
|
||||
});
|
||||
|
||||
test("viewer resets markers and drawing tools before committing a replacement image", async ({ page }) => {
|
||||
const editor = await setup(page, "viewer");
|
||||
const marker = editor.getByRole("textbox", { name: "Libellé du marqueur 1" });
|
||||
await marker.fill("Edited");
|
||||
await editor.getByTitle("Dessiner", { exact: true }).click();
|
||||
await editor.getByTitle("Gommer", { exact: true }).click();
|
||||
await page.evaluate(() => window.editorProbe.update({}));
|
||||
await expect(marker).toHaveValue("Edited");
|
||||
await expect(editor.getByTitle("Gommer", { exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||
const snapshots = await page.evaluate(() => {
|
||||
window.editorProbe.update({ image: {
|
||||
...window.viewerImage, drawingMode: "temporary",
|
||||
markers: [{ id: "b", label: "Second", x: 60, y: 60 }]
|
||||
} });
|
||||
return window.editorProbe.snapshots;
|
||||
});
|
||||
expect(snapshots.length).toBeGreaterThan(0);
|
||||
for (const values of snapshots) expect(values).toEqual(["Second"]);
|
||||
await expect(editor.getByTitle("Dessiner", { exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await editor.getByTitle("Dessiner", { exact: true }).click();
|
||||
await expect(editor.getByTitle("Gommer", { exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(editor.getByRole("button", { name: "Annotations temporaires" })).toBeVisible();
|
||||
await page.evaluate(() => window.editorProbe.unmount());
|
||||
});
|
||||
|
||||
test("cover ignores obsolete decodes and releases every bitmap and object URL", async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
const editor = await setup(page, "cover");
|
||||
const save = editor.getByRole("button", { name: "Valider", exact: true });
|
||||
await expect(save).toBeDisabled();
|
||||
expect(await page.evaluate(() => window.decoding.pending.length)).toBe(2);
|
||||
await page.evaluate(async () => {
|
||||
await window.decoding.resolve(0);
|
||||
await window.decoding.resolve(1);
|
||||
});
|
||||
await expect(save).toBeEnabled();
|
||||
await expect(editor.locator("img")).toBeVisible();
|
||||
await editor.getByLabel("Zoom", { exact: true }).press("End");
|
||||
await save.click();
|
||||
expect(await page.evaluate(() => window.editorProbe.saves[0])).toMatch(/^data:image\/(webp|jpeg);base64,/);
|
||||
await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() }));
|
||||
await expect(save).toBeDisabled();
|
||||
await expect(editor.locator("img")).toHaveCount(0);
|
||||
await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() }));
|
||||
await page.evaluate(async () => {
|
||||
await window.decoding.resolve(3);
|
||||
window.decoding.reject(2);
|
||||
});
|
||||
await expect(save).toBeEnabled();
|
||||
await expect(editor.locator(".toolbox-cover-editor-error")).toHaveCount(0);
|
||||
await page.evaluate(() => window.editorProbe.update({ file: window.probeFile.slice() }));
|
||||
await page.evaluate(() => window.decoding.reject(4));
|
||||
await expect(save).toBeDisabled();
|
||||
await expect(editor.locator(".toolbox-cover-editor-error")).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
window.editorProbe.update({ file: window.probeFile.slice() });
|
||||
window.editorProbe.unmount();
|
||||
});
|
||||
await page.evaluate(() => window.decoding.resolve(5));
|
||||
const resources = await page.evaluate(() => {
|
||||
window.decoding.restore();
|
||||
return { created: window.decoding.created, revoked: window.decoding.revoked, closed: window.decoding.closed };
|
||||
});
|
||||
expect(resources.revoked.sort()).toEqual(resources.created.sort());
|
||||
expect(resources.closed.sort()).toEqual([0, 1, 3, 5]);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("cover loads an existing image and clears it when the source is removed", async ({ page }) => {
|
||||
const editor = await setup(page, "cover", { existingCover: true });
|
||||
await expect.poll(() => page.evaluate(() => window.decoding.pending.length)).toBe(2);
|
||||
await page.evaluate(async () => {
|
||||
await window.decoding.resolve(0);
|
||||
await window.decoding.resolve(1);
|
||||
});
|
||||
await expect(editor.getByRole("button", { name: "Valider", exact: true })).toBeEnabled();
|
||||
await page.evaluate(() => window.editorProbe.update({ existingCover: "" }));
|
||||
await expect(editor.locator("img")).toHaveCount(0);
|
||||
await expect(editor.getByRole("button", { name: "Valider", exact: true })).toBeDisabled();
|
||||
const resources = await page.evaluate(() => {
|
||||
window.editorProbe.unmount();
|
||||
window.decoding.restore();
|
||||
return { created: window.decoding.created, closed: window.decoding.closed };
|
||||
});
|
||||
expect(resources.created).toEqual([]);
|
||||
expect(resources.closed.sort()).toEqual([0, 1]);
|
||||
});
|
||||
85
e2e/fixtures/editor-lifecycle-probe.js
Normal file
85
e2e/fixtures/editor-lifecycle-probe.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Rôle : monte les éditeurs réels sous StrictMode et contrôle les décodages concurrents.
|
||||
/* global URL */
|
||||
import { createElement, StrictMode, useLayoutEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import { ImageViewer } from "../../website/src/components/ImageViewer.jsx";
|
||||
import { ToolboxCoverEditor } from "../../website/src/features/toolboxes/ToolboxCoverEditor.jsx";
|
||||
import { RngSimulatorModule } from "../../website/src/features/toolboxes/modules/RngSimulatorModule.jsx";
|
||||
import { normalizeRngSimulatorData } from "../../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
export function mountEditorProbe(element, kind, initialProps) {
|
||||
const root = createRoot(element);
|
||||
const snapshots = [];
|
||||
const saves = [];
|
||||
let updateProps;
|
||||
const components = { viewer: ImageViewer, cover: ToolboxCoverEditor, rng: RngSimulatorModule };
|
||||
function Probe() {
|
||||
const [props, setProps] = useState(initialProps);
|
||||
updateProps = setProps;
|
||||
useLayoutEffect(() => {
|
||||
snapshots.push(Array.from(element.querySelectorAll("input"), (input) => input.value));
|
||||
});
|
||||
const context = {
|
||||
getModuleData: () => props.data,
|
||||
normalizeRngSimulatorData,
|
||||
setModuleData: (_toolbox, _module, data) => {
|
||||
saves.push(data);
|
||||
setProps((current) => ({ ...current, data }));
|
||||
},
|
||||
uid: () => "new-rate"
|
||||
};
|
||||
return createElement(components[kind], {
|
||||
toolboxId: "probe", moduleId: "rng", context,
|
||||
onClose: () => {}, onSave: (value) => saves.push(value),
|
||||
setEditing: (editing) => setProps((current) => ({ ...current, editing })),
|
||||
...props
|
||||
});
|
||||
}
|
||||
flushSync(() => root.render(createElement(StrictMode, null, createElement(Probe))));
|
||||
return {
|
||||
snapshots, saves,
|
||||
update(patch) {
|
||||
snapshots.length = 0;
|
||||
flushSync(() => updateProps((props) => ({ ...props, ...patch })));
|
||||
},
|
||||
unmount: () => flushSync(() => root.unmount())
|
||||
};
|
||||
}
|
||||
|
||||
export function controlImageDecoding() {
|
||||
const originalDecode = globalThis.createImageBitmap;
|
||||
const originalCreate = URL.createObjectURL;
|
||||
const originalRevoke = URL.revokeObjectURL;
|
||||
const pending = [];
|
||||
const created = [];
|
||||
const revoked = [];
|
||||
const closed = [];
|
||||
globalThis.createImageBitmap = (file) => new Promise((resolve, reject) => {
|
||||
pending.push({ file, resolve, reject });
|
||||
});
|
||||
URL.createObjectURL = (file) => {
|
||||
const url = originalCreate(file);
|
||||
created.push(url);
|
||||
return url;
|
||||
};
|
||||
URL.revokeObjectURL = (url) => {
|
||||
revoked.push(url);
|
||||
originalRevoke(url);
|
||||
};
|
||||
return {
|
||||
pending, created, revoked, closed,
|
||||
async resolve(index) {
|
||||
const bitmap = await originalDecode(pending[index].file);
|
||||
const close = bitmap.close.bind(bitmap);
|
||||
bitmap.close = () => { closed.push(index); close(); };
|
||||
pending[index].resolve(bitmap);
|
||||
},
|
||||
reject: (index) => pending[index].reject(new Error("decode failed")),
|
||||
restore() {
|
||||
globalThis.createImageBitmap = originalDecode;
|
||||
URL.createObjectURL = originalCreate;
|
||||
URL.revokeObjectURL = originalRevoke;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -43,17 +43,19 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
const [eraseActive, setEraseActive] = useState(false);
|
||||
const [drawingColor, setDrawingColor] = useState("#f6c453");
|
||||
const [drawingWidth, setDrawingWidth] = useState(4);
|
||||
const [previousImage, setPreviousImage] = useState(image);
|
||||
const viewerRef = useRef(null);
|
||||
const mediaRef = useRef(null);
|
||||
const hasMarkers = viewerMarkers.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (previousImage !== image) {
|
||||
setPreviousImage(image);
|
||||
setViewerMarkers(Array.isArray(image.markers) ? image.markers : []);
|
||||
setViewerDrawings(image.drawings?.strokes ? image.drawings : { strokes: [] });
|
||||
setDrawingMode(image.drawingMode === "temporary" ? "temporary" : "permanent");
|
||||
setDrawingActive(false);
|
||||
setEraseActive(false);
|
||||
}, [image]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewerRef.current || !mediaRef.current) return undefined;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Rôle : édite une image utilisateur en cover rectangulaire de card toolbox.
|
||||
/* eslint-disable react-refresh/only-export-components -- expose aussi les helpers purs de calcul/export du crop. */
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "../../components/Icon.jsx";
|
||||
import { useKeyedState } from "../../hooks/useKeyedState.js";
|
||||
import { lockBodyScroll } from "../../utils/bodyScrollLock.js";
|
||||
|
||||
const OUTPUT_WIDTH = 620;
|
||||
|
|
@ -64,46 +65,43 @@ export function ToolboxCoverEditor({ file, existingCover = "", onClose, onSave }
|
|||
const frameRef = useRef(null);
|
||||
const imageBitmapRef = useRef(null);
|
||||
const dragRef = useRef(null);
|
||||
const [source, setSource] = useState("");
|
||||
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
|
||||
const [preview, setPreview] = useKeyedState(file || existingCover, () => ({ source: "", width: 0, height: 0, error: "" }));
|
||||
const { source, error } = preview;
|
||||
const imageSize = preview;
|
||||
const [frame, setFrame] = useState({ width: 0, height: 0 });
|
||||
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
|
||||
const [baseScale, setBaseScale] = useState(1);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const maxScale = useMemo(() => baseScale * 4, [baseScale]);
|
||||
const baseScale = frame.width && frame.height && imageSize.width && imageSize.height
|
||||
? Math.max(frame.width / imageSize.width, frame.height / imageSize.height) : 1;
|
||||
const transformKey = JSON.stringify([source, frame.width, frame.height, imageSize.width, imageSize.height]);
|
||||
const [transform, setTransform] = useKeyedState(transformKey, () => ({ x: 0, y: 0, scale: baseScale }));
|
||||
const maxScale = baseScale * 4;
|
||||
|
||||
useEffect(() => lockBodyScroll(), []);
|
||||
|
||||
useEffect(() => () => imageBitmapRef.current?.close?.(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSource = createPreviewSource(file, existingCover);
|
||||
setSource(nextSource);
|
||||
setError("");
|
||||
return () => {
|
||||
if (file && nextSource) URL.revokeObjectURL(nextSource);
|
||||
};
|
||||
}, [existingCover, file]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!source) return undefined;
|
||||
if (!nextSource) return undefined;
|
||||
let cancelled = false;
|
||||
decodePreviewImage(source, file)
|
||||
let decodedBitmap;
|
||||
decodePreviewImage(nextSource, file)
|
||||
.then((bitmap) => {
|
||||
if (cancelled) {
|
||||
bitmap.close?.();
|
||||
return;
|
||||
}
|
||||
imageBitmapRef.current?.close?.();
|
||||
decodedBitmap = bitmap;
|
||||
imageBitmapRef.current = bitmap;
|
||||
setImageSize({ width: bitmap.width, height: bitmap.height });
|
||||
setPreview({ source: nextSource, width: bitmap.width, height: bitmap.height, error: "" });
|
||||
})
|
||||
.catch(() => setError("Image illisible. Choisis un autre fichier."));
|
||||
.catch(() => {
|
||||
if (!cancelled) setPreview({ source: "", width: 0, height: 0, error: "Image illisible. Choisis un autre fichier." });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
decodedBitmap?.close?.();
|
||||
if (imageBitmapRef.current === decodedBitmap) imageBitmapRef.current = null;
|
||||
if (file) URL.revokeObjectURL(nextSource);
|
||||
};
|
||||
}, [file, source]);
|
||||
}, [existingCover, file, setPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
const frameElement = frameRef.current;
|
||||
|
|
@ -118,13 +116,6 @@ export function ToolboxCoverEditor({ file, existingCover = "", onClose, onSave }
|
|||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frame.width || !frame.height || !imageSize.width || !imageSize.height) return;
|
||||
const nextBaseScale = Math.max(frame.width / imageSize.width, frame.height / imageSize.height);
|
||||
setBaseScale(nextBaseScale);
|
||||
setTransform({ x: 0, y: 0, scale: nextBaseScale });
|
||||
}, [frame.height, frame.width, imageSize.height, imageSize.width]);
|
||||
|
||||
function clampTransform(nextTransform) {
|
||||
const displayWidth = imageSize.width * nextTransform.scale;
|
||||
const displayHeight = imageSize.height * nextTransform.scale;
|
||||
|
|
|
|||
|
|
@ -165,10 +165,20 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
const [occurrenceDraft, setOccurrenceDraft] = useState(null);
|
||||
const [realDrawsDraft, setRealDrawsDraft] = useState(null);
|
||||
const rateFormRef = useRef(null);
|
||||
const previousEditingRef = useRef(editing);
|
||||
const [previousEditing, setPreviousEditing] = useState(editing);
|
||||
const rateDraft = useDraftForm({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
const rateFormOpen = editing || Boolean(editingRateId);
|
||||
const activeRate = data.rates.find((rate) => rate.id === data.activeRateId) || data.rates[0] || null;
|
||||
const draftKey = JSON.stringify([toolboxId, moduleId, activeRate?.id, activeRate?.occurrenceUnit]);
|
||||
const [previousDraftKey, setPreviousDraftKey] = useState(null);
|
||||
if (previousDraftKey !== draftKey) {
|
||||
setPreviousDraftKey(draftKey);
|
||||
setMultiplierDraft(null);
|
||||
setCostDraft(null);
|
||||
setOccurrenceDraft(null);
|
||||
setRealDrawsDraft(null);
|
||||
setOccurrenceUnit(activeRate?.occurrenceUnit || "perHour");
|
||||
}
|
||||
const summary = useMemo(() => activeRate ? calculateRngSummary({
|
||||
rate: activeRate.rate,
|
||||
multiplier: activeRate.multiplier,
|
||||
|
|
@ -197,14 +207,11 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
const averageExceededDraws = Math.max(0, Math.round(averageDrawsDelta));
|
||||
const averageDrawsTone = realDrawsSummary?.averageExceeded ? (averageExceededDraws > 0 ? "negative" : "") : "positive";
|
||||
|
||||
useEffect(() => {
|
||||
const wasEditing = previousEditingRef.current;
|
||||
if (wasEditing && !editing) setEditingRateId("");
|
||||
if (!wasEditing && editing && !editingRateId) rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
previousEditingRef.current = editing;
|
||||
// Le hook useDraftForm expose reset sans identité stable; cette transition ne doit dépendre que de l'ouverture.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editing, editingRateId]);
|
||||
if (previousEditing !== editing) {
|
||||
setPreviousEditing(editing);
|
||||
if (!editing) setEditingRateId("");
|
||||
if (editing && !editingRateId) rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!rateFormOpen) return undefined;
|
||||
|
|
@ -219,14 +226,6 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
|
||||
}, [rateFormOpen, setEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplierDraft(null);
|
||||
setCostDraft(null);
|
||||
setOccurrenceDraft(null);
|
||||
setRealDrawsDraft(null);
|
||||
setOccurrenceUnit(activeRate?.occurrenceUnit || "perHour");
|
||||
}, [activeRate?.id, activeRate?.occurrenceUnit]);
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue