feat(ui): action bar is contextualizez accross sections
- [download] + [remove from recent] for recent section - [download] + [remove from favorite] for favorite section - [restore] + [permanent delete] for trash section
This commit is contained in:
@@ -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));
|
||||
</script>
|
||||
|
||||
@@ -352,25 +331,25 @@
|
||||
}}
|
||||
>
|
||||
{#snippet batchActions(sel)}
|
||||
<!--
|
||||
Favorites-scoped batch cluster: Download stays. Move + Delete
|
||||
were destructive-to-content operations carried over from the
|
||||
pre-refactor menu; on a favorites *bookmarks* view they
|
||||
belong in the row's context menu (rename/move/delete via
|
||||
`contextActions`), not in the batch bar. Batch "remove from
|
||||
favorite" un-stars the selected rows without touching the
|
||||
underlying files — mirrors the per-row favorite star.
|
||||
-->
|
||||
<Button
|
||||
icon="download"
|
||||
data-testid="favorites-batch-download-btn"
|
||||
onclick={() => batchDownload(sel)}>{t('common.download', 'Download')}</Button
|
||||
>
|
||||
<Button
|
||||
icon="arrows-alt"
|
||||
data-testid="favorites-batch-move-btn"
|
||||
onclick={() => {
|
||||
moveTarget = null;
|
||||
moveItems = batchTargets(sel);
|
||||
moveOpen = true;
|
||||
}}>{t('files.move', 'Move')}</Button
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
data-testid="favorites-batch-delete-btn"
|
||||
onclick={() => batchDelete(sel)}>{t('common.delete', 'Delete')}</Button
|
||||
icon="star-outline"
|
||||
data-testid="favorites-batch-remove-btn"
|
||||
onclick={() => sel.forEach(unfavorite)}
|
||||
>{t('files.unfavorite', 'Remove favorite')}</Button
|
||||
>
|
||||
{/snippet}
|
||||
</ResourceList>
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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)}
|
||||
<!--
|
||||
Recent-scoped batch cluster: what makes sense on a HISTORY
|
||||
view. Download stays (common bulk fetch). Move + Delete
|
||||
were destructive-to-content actions carried over from the
|
||||
pre-refactor menu; on a history view they belong in the
|
||||
row's context menu (rename/move/delete via `contextActions`
|
||||
above), not in the batch bar. Batch "remove from recent"
|
||||
mirrors the per-row broom and forgets the selected rows
|
||||
from history without touching the files themselves.
|
||||
-->
|
||||
<Button
|
||||
icon="download"
|
||||
data-testid="recent-batch-download-btn"
|
||||
onclick={() => batchDownload(sel)}>{t('common.download', 'Download')}</Button
|
||||
>
|
||||
<Button
|
||||
icon="arrows-alt"
|
||||
data-testid="recent-batch-move-btn"
|
||||
onclick={() => {
|
||||
moveTarget = null;
|
||||
moveItems = batchTargets(sel);
|
||||
moveOpen = true;
|
||||
}}>{t('files.move', 'Move')}</Button
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
data-testid="recent-batch-delete-btn"
|
||||
onclick={() => batchDelete(sel)}>{t('common.delete', 'Delete')}</Button
|
||||
icon="broom"
|
||||
data-testid="recent-batch-remove-btn"
|
||||
onclick={() => sel.forEach(removeItem)}
|
||||
>{t('recent.remove_item', 'Remove from recent')}</Button
|
||||
>
|
||||
{/snippet}
|
||||
{#snippet itemActions(item)}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)}
|
||||
<button
|
||||
class="btn-action"
|
||||
<!--
|
||||
Use the shared `<Button>` component here (not the icon-only
|
||||
`.btn-action` chip used for per-row `itemActions` above). The
|
||||
batch bar renders text next to the glyph — `.btn-action` is
|
||||
fixed 28x28 with no room for a label, and shoving text
|
||||
inside was overlapping the icon. `<Button>` picks up the
|
||||
standard action-bar sizing and reads consistently with
|
||||
`/recent` and `/favorites` batch clusters.
|
||||
-->
|
||||
<Button
|
||||
icon="undo"
|
||||
data-testid="trash-batch-restore-btn"
|
||||
title={t('trash.restore', 'Restore')}
|
||||
onclick={() => sel.forEach(restore)}
|
||||
onclick={() => sel.forEach(restore)}>{t('trash.restore', 'Restore')}</Button
|
||||
>
|
||||
<Icon name="undo" />
|
||||
{t('trash.restore', 'Restore')}
|
||||
</button>
|
||||
<button
|
||||
class="btn-action btn-action--delete"
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
data-testid="trash-batch-delete-btn"
|
||||
title={t('trash.delete', 'Delete permanently')}
|
||||
onclick={() => sel.forEach(purge)}
|
||||
>{t('trash.delete', 'Delete permanently')}</Button
|
||||
>
|
||||
<Icon name="trash" />
|
||||
{t('trash.delete', 'Delete permanently')}
|
||||
</button>
|
||||
{/snippet}
|
||||
{#snippet rowBadge(_item, ctx)}
|
||||
{@const chip = expiryChip(ctx?.date)}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user