feat(ui): undo for deleted label elements, warn before leaving unsaved changes
The Delete key removed a label element for good; it now shows a toast with "Rückgängig" that puts it back in place. Closing or reloading the designer with unsaved changes asks first. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b4c8d80c33
commit
d57162e709
1 file changed
+37
-1
+37
-1
@@ -184,9 +184,25 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deleting is one key press away, so it is always undoable.
|
||||||
function deleteElement(id: string) {
|
function deleteElement(id: string) {
|
||||||
|
const index = elements.findIndex((element) => element.id === id);
|
||||||
|
const removed = elements[index];
|
||||||
|
if (!removed) return;
|
||||||
setElements((current) => current.filter((element) => element.id !== id));
|
setElements((current) => current.filter((element) => element.id !== id));
|
||||||
setSelectedId((current) => (current === id ? null : current));
|
setSelectedId((current) => (current === id ? null : current));
|
||||||
|
toast("Element gelöscht", {
|
||||||
|
action: {
|
||||||
|
label: "Rückgängig",
|
||||||
|
onClick: () =>
|
||||||
|
setElements((current) => [
|
||||||
|
...current.slice(0, index),
|
||||||
|
removed,
|
||||||
|
...current.slice(index),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateElement(id: string) {
|
function duplicateElement(id: string) {
|
||||||
@@ -210,8 +226,28 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP
|
|||||||
}
|
}
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
// deleteElement only reads `elements`, which is listed.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedId, elements]);
|
}, [selectedId, elements]);
|
||||||
|
|
||||||
|
// Leaving the page (tab close, reload) with unsaved changes asks first.
|
||||||
|
const currentSnapshot = JSON.stringify({ name, elements, widthMm, heightMm });
|
||||||
|
const initialSnapshot = JSON.stringify({
|
||||||
|
name: initial?.name ?? "",
|
||||||
|
elements: initial?.elements ?? [],
|
||||||
|
widthMm: initial?.widthMm ?? 90,
|
||||||
|
heightMm: initial?.heightMm ?? 35,
|
||||||
|
});
|
||||||
|
const hasUnsavedChanges = currentSnapshot !== initialSnapshot;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasUnsavedChanges || isSubmitting) return;
|
||||||
|
function onBeforeUnload(event: BeforeUnloadEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
window.addEventListener("beforeunload", onBeforeUnload);
|
||||||
|
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
||||||
|
}, [hasUnsavedChanges, isSubmitting]);
|
||||||
|
|
||||||
function handlePlaceholderInsert(key: PlaceholderKey) {
|
function handlePlaceholderInsert(key: PlaceholderKey) {
|
||||||
addElement(newTextElement({ placeholderKey: key, text: "" }));
|
addElement(newTextElement({ placeholderKey: key, text: "" }));
|
||||||
}
|
}
|
||||||
@@ -640,7 +676,7 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Element im Etikett anklicken, um es zu bearbeiten. Entf/Backspace löscht das
|
Element im Etikett anklicken, um es zu bearbeiten. Entf/Backspace löscht das
|
||||||
ausgewählte Element.
|
ausgewählte Element – „Rückgängig“ im Hinweis holt es zurück.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in new issue
Block a user