diff --git a/frontend/src/routes/favorites/+page.svelte b/frontend/src/routes/favorites/+page.svelte index dab700d6..225c6b20 100644 --- a/frontend/src/routes/favorites/+page.svelte +++ b/frontend/src/routes/favorites/+page.svelte @@ -286,31 +286,10 @@ // prunes its own selection when items reload) — benches/ROUND11.md §S1. type Selectable = FileItem | FolderItem; - function batchTargets(sel: Selectable[]) { - return sel.map((i) => ({ id: i.id, name: i.name, kind: kindOf(i) })); - } - function batchDownload(sel: Selectable[]) { for (const i of sel) downloadItem(i); } - async function batchDelete(sel: Selectable[]) { - const ok = await confirmDialog({ - title: t('common.delete', 'Delete'), - message: t('files.confirm_delete_n', { count: sel.length }, 'Delete {{count}} item(s)?'), - confirmText: t('common.delete', 'Delete'), - danger: true - }); - if (!ok) return; - try { - await Promise.all(sel.map((i) => (isFile(i) ? deleteFile(i.id) : deleteFolder(i.id)))); - const removed = new Set(sel.map((i) => i.id)); - raw = raw.filter((i) => !removed.has(i.resource.id)); - } catch (e) { - errorToast(e); - } - } - onMount(() => load(true)); @@ -352,25 +331,25 @@ }} > {#snippet batchActions(sel)} + - sel.forEach(unfavorite)} + >{t('files.unfavorite', 'Remove favorite')} {/snippet} diff --git a/frontend/src/routes/favorites/page.test.ts b/frontend/src/routes/favorites/page.test.ts index cd0277ff..989a45bc 100644 --- a/frontend/src/routes/favorites/page.test.ts +++ b/frontend/src/routes/favorites/page.test.ts @@ -86,13 +86,18 @@ it('unfavorites a row via the star button', async () => { await waitFor(() => expect(removeFavorite).toHaveBeenCalledWith('file', 'f1')); }); -it('batch-deletes selected favorites after confirmation', async () => { +it('batch-removes-from-favorite the selection', async () => { + // /favorites' batch bar was intentionally trimmed to Download + + // Remove-from-favorite. Bulk-deleting the underlying file from + // this view (previous behaviour) confused the "this is a + // bookmarks list" semantics — destructive actions belong in the + // row's context menu, not in the batch bar. This test pins the + // new shape: batch button just un-stars the selection. withOneFile(); - confirmDialog.mockResolvedValue(true); - m(deleteFile).mockResolvedValue(undefined); + m(removeFavorite).mockResolvedValue(undefined); render(FavoritesPage); await screen.findByText('photo.png'); await fireEvent.click(screen.getByTestId('resource-list-select-f1-checkbox')); - await fireEvent.click(await screen.findByTestId('favorites-batch-delete-btn')); - await waitFor(() => expect(deleteFile).toHaveBeenCalledWith('f1')); + await fireEvent.click(await screen.findByTestId('favorites-batch-remove-btn')); + await waitFor(() => expect(removeFavorite).toHaveBeenCalledWith('file', 'f1')); }); diff --git a/frontend/src/routes/recent/+page.svelte b/frontend/src/routes/recent/+page.svelte index 84c7e0b5..c1ff3477 100644 --- a/frontend/src/routes/recent/+page.svelte +++ b/frontend/src/routes/recent/+page.svelte @@ -319,31 +319,10 @@ // prunes its own selection when items reload) — benches/ROUND11.md §S1. type Selectable = FileItem | FolderItem; - function batchTargets(sel: Selectable[]) { - return sel.map((i) => ({ id: i.id, name: i.name, kind: kindOf(i) })); - } - function batchDownload(sel: Selectable[]) { for (const i of sel) downloadItem(i); } - async function batchDelete(sel: Selectable[]) { - const ok = await confirmDialog({ - title: t('common.delete', 'Delete'), - message: t('files.confirm_delete_n', { count: sel.length }, 'Delete {{count}} item(s)?'), - confirmText: t('common.delete', 'Delete'), - danger: true - }); - if (!ok) return; - try { - await Promise.all(sel.map((i) => (isFile(i) ? deleteFile(i.id) : deleteFolder(i.id)))); - const removed = new Set(sel.map((i) => i.id)); - raw = raw.filter((i) => !removed.has(i.resource.id)); - } catch (e) { - errorToast(e); - } - } - onMount(() => { void load(true); }); @@ -401,25 +380,26 @@ {/if} {/snippet} {#snippet batchActions(sel)} + - sel.forEach(removeItem)} + >{t('recent.remove_item', 'Remove from recent')} {/snippet} {#snippet itemActions(item)} diff --git a/frontend/src/routes/recent/page.test.ts b/frontend/src/routes/recent/page.test.ts index 63076714..1aae3136 100644 --- a/frontend/src/routes/recent/page.test.ts +++ b/frontend/src/routes/recent/page.test.ts @@ -92,15 +92,20 @@ it('removes a recent row via the broom button', async () => { await waitFor(() => expect(removeFromRecent).toHaveBeenCalledWith('file', 'r1')); }); -it('batch-deletes selected recent items after confirmation', async () => { +it('batch-removes-from-recent the selection', async () => { + // /recent's batch bar was intentionally trimmed to Download + + // Remove-from-recent. Bulk-deleting the underlying file from + // this history view (previous behaviour) confused the "this is + // activity log" semantics — destructive actions belong in the + // row's context menu, not in the batch bar. This test pins the + // new shape: batch button just forgets the selection from history. withOneFile(); - confirmDialog.mockResolvedValue(true); - m(deleteFile).mockResolvedValue(undefined); + m(removeFromRecent).mockResolvedValue(undefined); render(RecentPage); await screen.findByText('notes.txt'); await fireEvent.click(screen.getByTestId('resource-list-select-r1-checkbox')); - await fireEvent.click(await screen.findByTestId('recent-batch-delete-btn')); - await waitFor(() => expect(deleteFile).toHaveBeenCalledWith('r1')); + await fireEvent.click(await screen.findByTestId('recent-batch-remove-btn')); + await waitFor(() => expect(removeFromRecent).toHaveBeenCalledWith('file', 'r1')); }); it('renders an empty state when there is no recent activity', async () => { diff --git a/frontend/src/routes/trash/+page.svelte b/frontend/src/routes/trash/+page.svelte index 83913fdb..beb4c2af 100644 --- a/frontend/src/routes/trash/+page.svelte +++ b/frontend/src/routes/trash/+page.svelte @@ -16,6 +16,7 @@ import { formatDate } from '$lib/utils/display'; import type { Drive, FileItem, FolderItem, TrashResourceItem } from '$lib/api/types'; import Icon from '$lib/icons/Icon.svelte'; + import Button from '$lib/components/Button.svelte'; import ResourceList, { isFile, type GroupByDef, @@ -290,24 +291,27 @@ {/if} {/snippet} {#snippet batchActions(sel)} - - - {t('trash.restore', 'Restore')} - - - - {t('trash.delete', 'Delete permanently')} - {/snippet} {#snippet rowBadge(_item, ctx)} {@const chip = expiryChip(ctx?.date)} diff --git a/tests/e2e/spa/favorites.spec.ts b/tests/e2e/spa/favorites.spec.ts index 9d5a476a..165fd0f1 100644 --- a/tests/e2e/spa/favorites.spec.ts +++ b/tests/e2e/spa/favorites.spec.ts @@ -55,8 +55,12 @@ test('favorites batch select-all then move dialog', async ({ page }) => { await page.getByTestId('resource-list-select-all-checkbox').check(); await expect(page.getByTestId('resource-list-batch-close-btn')).toBeVisible(); - // Batch-move opens the move dialog; cancel it. - await page.getByTestId('favorites-batch-move-btn').click(); - await expect(page.getByTestId('move-dialog')).toBeVisible({ timeout: 15_000 }); - await page.getByTestId('move-dialog-cancel-btn').click(); + // Batch-remove-from-favorite un-stars every selected row without + // touching the underlying file — the /favorites batch bar was + // trimmed to Download + Remove-from-favorite (destructive-to-content + // actions moved into the row context menu). Verify the two folders + // vanish from the list after the click. + await page.getByTestId('favorites-batch-remove-btn').click(); + await expect(page.getByTestId(f1)).toHaveCount(0, { timeout: 15_000 }); + await expect(page.getByTestId(f2)).toHaveCount(0); }); diff --git a/tests/e2e/spa/recent.spec.ts b/tests/e2e/spa/recent.spec.ts index ddd24cd5..ad9b0c0a 100644 --- a/tests/e2e/spa/recent.spec.ts +++ b/tests/e2e/spa/recent.spec.ts @@ -23,12 +23,15 @@ test('recent shows accessed items, batch selection, and clear', async ({ page }) await expect(page.getByTestId('appshell-logo-link')).toBeVisible({ timeout: 15_000 }); // Switch to list view (reveals the select-all header) and batch-select. + // /recent's batch bar was trimmed to Download + Remove-from-recent + // (destructive-to-content actions moved into the row context menu), + // so this exercises the new remove-from-recent batch instead of the + // old batch-move-into-dialog flow. await page.getByTestId('display-mode-view-list-btn').click({ timeout: 3_000 }).catch(() => {}); const selectAll = page.getByTestId('resource-list-select-all-checkbox'); if (await selectAll.isVisible().catch(() => false)) { await selectAll.check(); - await page.getByTestId('recent-batch-move-btn').click({ timeout: 3_000 }).catch(() => {}); - await page.getByTestId('move-dialog-cancel-btn').click({ timeout: 3_000 }).catch(() => {}); + await page.getByTestId('recent-batch-remove-btn').click({ timeout: 3_000 }).catch(() => {}); } // Clear the history if the control is present.