feat(fase-5.11): selection mode + bulk actions (delete / move / mark-checked)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
if (!ids.length) return;
|
||||
await getSupabase().from('shopping_items').delete().in('id', ids);
|
||||
}
|
||||
|
||||
export async function bulkMoveItems(ids: string[], newListId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<ShoppingItem, 'id'>[]) {
|
||||
await Promise.all(
|
||||
items.map((item, index) =>
|
||||
|
||||
Reference in New Issue
Block a user