group delete with confirmation + undo, retry progress bar fix
context-menu: group delete (public/js/modules/context-menu.js):
* When the delete action targets a card that has children (a group
parent), the confirmation dialog is ALWAYS shown regardless of the
hyprcrd-confirm-delete setting — group deletion is destructive
enough that it should never happen accidentally. The dialog lists
every child card by title and the OK button reads
"Delete all N" (parent + children counted).
* Children are deleted first (avoids orphan flicker), then the parent.
* Undo: a single pushUndo entry recreates the parent via POST first,
then all children with parent_id pointing at the new parent id.
due_date is restored via a follow-up PATCH since POST doesn't
accept it. Ctrl+Z fully restores the group hierarchy.
* Non-group cards keep the existing behavior (simple confirm if the
setting is on, single delete, pushCardDelete for undo).
tests (public/plugins/tests/card-interactions.test.js):
* New describe "Card: Group Delete" with two tests:
- "deleting a group removes parent AND all children": creates a
parent + 2 children, right-clicks the parent, clicks delete,
asserts the dialog text contains both child titles, confirms,
verifies all 3 cards are gone from the API.
- "Ctrl+Z after group delete restores parent + children": creates
parent + 1 child, deletes the group, confirms both gone, fires
Ctrl+Z, finds the restored cards by title, asserts
child.parent_id === restoredParent.id.
Both tests save/restore the hyprcrd-confirm-delete localStorage
value and soft-skip if the parent isn't visible on the board.
runner: retry progress bar (public/plugins/tests/runner.js):
* hideProgress() was called inside runAll() at the end of the first
pass, which removed the #test-progress element before the retry
pass started. The retry ran invisibly — "retrying…" never showed.
Moved hideProgress() out of runAll() into run(), after both the
first pass and the retry pass are done. The bar now stays visible
for the whole sequence: first pass fills to 100%, retry resets the
fill to 0 (via updateProgress(0, total, 'retrying…', false)) and
fills again for just the failed tests.
chore: bump version to 0.9.10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c16659c2ad
commit
56c90225cb
4 files changed
+168
-6
No files matched your search
@@ -354,13 +354,49 @@ export function showContextMenu(x, y, card) {
|
||||
else if (a === 'ungroup') { hideContextMenu(); pushCardEdit('ungroup', card.id, { parent_id: card.parent_id }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { parent_id: null } }); await window._reloadAll(); }
|
||||
else if (a === 'delete') {
|
||||
hideContextMenu();
|
||||
const children = getChildren(card.id);
|
||||
const isGroup = children.length > 0;
|
||||
const needConfirm = localStorage.getItem('hyprcrd-confirm-delete') !== '0';
|
||||
if (needConfirm) {
|
||||
if (isGroup) {
|
||||
// Always confirm when deleting a group — list every child
|
||||
// so the user knows exactly what they're losing.
|
||||
const childList = children.map(c => `• ${c.title || '(untitled)'}`).join('\n');
|
||||
const msg = `Delete group "${card.title}" and all ${children.length} card${children.length > 1 ? 's' : ''} inside?\n\n${childList}`;
|
||||
const ok = await showDialog({ title: 'delete_group', message: msg, danger: true, okLabel: `Delete all ${children.length + 1}` });
|
||||
if (!ok) return;
|
||||
} else if (needConfirm) {
|
||||
const ok = await showDialog({ title: 'delete_card', message: `Really delete "${card.title}"?`, danger: true, okLabel: 'Delete' });
|
||||
if (!ok) return;
|
||||
}
|
||||
const wasSync = !!card.caldav_uid;
|
||||
pushCardDelete('delete', snapshotCard(card));
|
||||
// Snapshot everything for undo BEFORE deleting
|
||||
const allCards = isGroup ? [card, ...children] : [card];
|
||||
const snapshots = allCards.map(c => snapshotCard(c));
|
||||
const wasSync = allCards.some(c => !!c.caldav_uid);
|
||||
pushUndo(`delete ${isGroup ? 'group' : 'card'} "${card.title}"`, async () => {
|
||||
// Recreate parent first so children can reference it
|
||||
const parent = await api('api/cards', { method: 'POST', body: {
|
||||
title: snapshots[0].title, body: snapshots[0].body,
|
||||
color: snapshots[0].color, bucket_id: snapshots[0].bucket_id,
|
||||
position: snapshots[0].position,
|
||||
}});
|
||||
if (parent?.id && snapshots[0].due_date) {
|
||||
await api(`api/cards/${parent.id}`, { method: 'PATCH', body: { due_date: snapshots[0].due_date } });
|
||||
}
|
||||
// Recreate children pointing at the new parent
|
||||
for (let i = 1; i < snapshots.length; i++) {
|
||||
const s = snapshots[i];
|
||||
const child = await api('api/cards', { method: 'POST', body: {
|
||||
title: s.title, body: s.body, color: s.color,
|
||||
bucket_id: s.bucket_id, parent_id: parent?.id,
|
||||
position: s.position,
|
||||
}});
|
||||
if (child?.id && s.due_date) {
|
||||
await api(`api/cards/${child.id}`, { method: 'PATCH', body: { due_date: s.due_date } });
|
||||
}
|
||||
}
|
||||
});
|
||||
// Delete children first, then parent (avoids orphan flicker)
|
||||
for (const c of children) await api(`api/cards/${c.id}`, { method: 'DELETE' });
|
||||
await api(`api/cards/${card.id}`, { method: 'DELETE' });
|
||||
await window._reloadAll();
|
||||
if (wasSync) window._triggerSync?.();
|
||||
|
||||
Reference in new issue
Block a user