From f3e8234b7082d28992b2c1b52905dfc96d02debe Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 13 Apr 2026 19:33:46 +0200 Subject: [PATCH] feat(fase-5.11): selection mode + bulk actions (delete / move / mark-checked) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-press on mobile or the new header toggle on desktop puts /lists/[id] into multi-select mode. Rows render a checkbox on the left, the stepper + drag handle go away, and swipes + double-tap are disabled. Chrome during selection mode Mobile: header turns into a dark slate-900 action bar with "N selected" + Cancel on the left, and a bottom action bar (Delete / Move / Check) appears above the BottomTabBar. The sticky add-item form is hidden. Desktop: same dark header with Cancel + inline action icons on the right. Bulk actions Delete — one scheduleUndoable with a batch restore (re-inserts all snapshots) / batch commit (bulkDeleteItems). Single undo toast. Move — opens a bottom sheet / dialog with the collective's other active lists + a "Create new list" row that creates an empty list and moves into it in a single gesture. Mark checked — flips only the selected unchecked items (bulkCheckItems with is_checked=false filter), no toggle. Store helpers (apps/web/src/lib/stores/lists.ts) bulkDeleteItems(ids) → DELETE .in('id', ids) bulkMoveItems(ids, newListId) → UPDATE list_id .in('id', ids) bulkCheckItems(ids, userId) → UPDATE is_checked=true where !is_checked Entry / exit Long-press 500 ms on a row enters with that row pre-selected. Movement cancels the long-press intent so it never fights the swipe. Single-tap toggles selection while active (instead of invoking the no-op short-tap / dbltap-overlay path). Cancel button exits + clears selection. Tests apps/web/tests/e2e/selection.test.ts (3 desktop tests) SEL-01 toggle enters, row click toggles, Cancel exits SEL-02 bulk delete → row gone + undo toast visible SEL-03 mark checked → row gains strikethrough Mobile long-press tests deferred until WebKit install lands (same reason as the swipe-toggle M-series). i18n additions list_select, list_cancel_selection, list_selection_count({n}), list_bulk_delete/move/mark_checked, list_undo_bulk_delete({n}), list_move_title, list_move_create_new (en/es). Verification just test-e2e → 43 passed + 2 skipped (was 40 + 2). Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/messages/en.json | 11 +- apps/web/messages/es.json | 11 +- apps/web/src/lib/stores/lists.ts | 21 + .../src/routes/(app)/lists/[id]/+page.svelte | 518 ++++++++++++++---- apps/web/tests/e2e/selection.test.ts | 95 ++++ 5 files changed, 559 insertions(+), 97 deletions(-) create mode 100644 apps/web/tests/e2e/selection.test.ts diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 981d278..33ca668 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -170,5 +170,14 @@ "list_reorder_handle": "Reorder", "list_edit_item": "Edit item", "list_confirm": "Confirm", - "list_close": "Close" + "list_close": "Close", + "list_select": "Select", + "list_selection_count": "{n} selected", + "list_cancel_selection": "Cancel", + "list_bulk_delete": "Delete", + "list_bulk_move": "Move", + "list_bulk_mark_checked": "Mark checked", + "list_undo_bulk_delete": "Deleted {n} items", + "list_move_title": "Move to list", + "list_move_create_new": "Create new list" } diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json index ad095b2..f55b254 100644 --- a/apps/web/messages/es.json +++ b/apps/web/messages/es.json @@ -170,5 +170,14 @@ "list_reorder_handle": "Reordenar", "list_edit_item": "Editar producto", "list_confirm": "Confirmar", - "list_close": "Cerrar" + "list_close": "Cerrar", + "list_select": "Seleccionar", + "list_selection_count": "{n} seleccionados", + "list_cancel_selection": "Cancelar", + "list_bulk_delete": "Eliminar", + "list_bulk_move": "Mover", + "list_bulk_mark_checked": "Marcar", + "list_undo_bulk_delete": "{n} eliminados", + "list_move_title": "Mover a lista", + "list_move_create_new": "Crear lista nueva" } diff --git a/apps/web/src/lib/stores/lists.ts b/apps/web/src/lib/stores/lists.ts index 9e1b3af..d64cac3 100644 --- a/apps/web/src/lib/stores/lists.ts +++ b/apps/web/src/lib/stores/lists.ts @@ -196,6 +196,27 @@ export async function deleteItem(id: string) { await getSupabase().from('shopping_items').delete().eq('id', id); } +// ── Bulk operations (selection mode) ────────────────────────────────────────── + +export async function bulkDeleteItems(ids: string[]): Promise { + if (!ids.length) return; + await getSupabase().from('shopping_items').delete().in('id', ids); +} + +export async function bulkMoveItems(ids: string[], newListId: string): Promise { + if (!ids.length) return; + await getSupabase().from('shopping_items').update({ list_id: newListId }).in('id', ids); +} + +export async function bulkCheckItems(ids: string[], userId: string): Promise { + if (!ids.length) return; + await getSupabase() + .from('shopping_items') + .update({ is_checked: true, checked_by: userId, checked_at: new Date().toISOString() }) + .in('id', ids) + .eq('is_checked', false); +} + export async function reorderItems(items: Pick[]) { await Promise.all( items.map((item, index) => diff --git a/apps/web/src/routes/(app)/lists/[id]/+page.svelte b/apps/web/src/routes/(app)/lists/[id]/+page.svelte index 949515c..e7c2603 100644 --- a/apps/web/src/routes/(app)/lists/[id]/+page.svelte +++ b/apps/web/src/routes/(app)/lists/[id]/+page.svelte @@ -1,5 +1,6 @@