Admin hybrid delete/retire of a quest definition (Delete Task) (#262)

Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
This commit was merged in pull request #262.
This commit is contained in:
2026-09-26 12:47:34 -04:00
committed by approver_agent
parent b1c14ed7d9
commit 02f4d8c8aa
11 changed files with 822 additions and 45 deletions
+92 -10
View File
@@ -13,12 +13,14 @@ import { ApiForbiddenError } from "../api/client";
import {
ApiRequestError,
createDefinition,
deleteDefinition,
fetchChildren,
fetchDefinitions,
previewOccurrences,
updateDefinition,
type AdminChild,
type DefinitionCreateBody,
type DefinitionDeleteResult,
type DefinitionEditBody,
type DefinitionRule,
type QuestDefinition,
@@ -273,6 +275,12 @@ function saveErrorMessage(error: unknown): string {
return "The task could not be saved. Check your connection and try again.";
}
function deleteErrorMessage(error: unknown): string {
if (error instanceof ApiForbiddenError) return error.message;
if (error instanceof ApiRequestError && error.detail) return error.detail;
return "The task could not be deleted. Check your connection and try again.";
}
function toggle<T>(list: T[], value: T): T[] {
return list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
}
@@ -449,11 +457,13 @@ function EditSheet({
activeChildren,
onCancel,
onSaved,
onDeleted,
}: {
target: "new" | QuestDefinition;
activeChildren: AdminChild[];
onCancel: () => void;
onSaved: () => void;
onDeleted: (result: DefinitionDeleteResult) => void;
}) {
const [draft, setDraft] = useState<Draft>(() =>
target === "new" ? newDraft(activeChildren) : draftFrom(target, activeChildren),
@@ -473,6 +483,7 @@ function EditSheet({
const [pickerOpen, setPickerOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [confirming, setConfirming] = useState(false);
const [confirmingDelete, setConfirmingDelete] = useState(false);
const [error, setError] = useState<string | null>(null);
// A ref, not state: two taps in one frame must not both see `saving === false`.
const inFlight = useRef(false);
@@ -490,8 +501,8 @@ function EditSheet({
}, []);
useEffect(() => {
if (confirming) confirmRef.current?.focus();
}, [confirming]);
if (confirming || confirmingDelete) confirmRef.current?.focus();
}, [confirming, confirmingDelete]);
function onKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
if (event.key === "Escape") {
@@ -559,6 +570,21 @@ function EditSheet({
}
}
async function remove() {
if (target === "new" || inFlight.current) return;
setConfirmingDelete(false);
inFlight.current = true;
setSaving(true);
setError(null);
try {
onDeleted(await deleteDefinition(target.id));
} catch (err) {
setError(deleteErrorMessage(err));
inFlight.current = false;
setSaving(false);
}
}
return (
<div className="defs-sheet-layer">
<div
@@ -906,15 +932,56 @@ function EditSheet({
</button>
</div>
</div>
) : (
<div className="defs-sheet-footer">
<button type="button" className="defs-cancel" onClick={onCancel} disabled={saving}>
Cancel
</button>
<button type="button" className="defs-save" onClick={() => save()} disabled={saving}>
{saving ? "Saving…" : "Save changes"}
</button>
) : confirmingDelete ? (
<div
className="defs-confirm"
role="group"
aria-labelledby="defs-delete-confirm-text"
data-testid="delete-confirm"
ref={confirmRef}
tabIndex={-1}
>
<p id="defs-delete-confirm-text">
Delete this task? If it has no completion history it is deleted; otherwise it is
retired (deactivated) and its history is kept.
</p>
<div className="defs-sheet-footer">
<button
type="button"
className="defs-cancel"
onClick={() => setConfirmingDelete(false)}
>
Go back
</button>
<button type="button" className="defs-save defs-save--danger" onClick={remove}>
Delete task
</button>
</div>
</div>
) : (
<>
<div className="defs-sheet-footer">
<button type="button" className="defs-cancel" onClick={onCancel} disabled={saving}>
Cancel
</button>
<button type="button" className="defs-save" onClick={() => save()} disabled={saving}>
{saving ? "Saving…" : "Save changes"}
</button>
</div>
{target !== "new" ? (
<button
type="button"
className="defs-delete"
disabled={saving}
onClick={() => {
setError(null);
setConfirmingDelete(true);
}}
>
Delete task
</button>
) : null}
</>
)}
</div>
</div>
@@ -1038,6 +1105,21 @@ export default function DefinitionsTab() {
setSheet(null);
setAttempt((n) => n + 1);
}}
onDeleted={(result) => {
setSheet(null);
setState((current) =>
current.kind !== "ready"
? current
: {
...current,
definitions: result.definition
? current.definitions.map((d) =>
d.id === result.definition_id ? result.definition! : d,
)
: current.definitions.filter((d) => d.id !== result.definition_id),
},
);
}}
/>
) : null}
</div>