fix: put plugin file actions behind a more button in the phone selection toolbar

This commit is contained in:
Linus Rath
2026-10-14 09:48:50 +02:00
parent 2b493f8f9c
commit d16edfe402
2 changed files with 35 additions and 4 deletions
@@ -17,6 +17,11 @@ import type { FileResourceView } from '@/lib/plugin-types';
vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
vi.mock('@/components/settings/share-collection-dialog', () => ({ ShareCollectionDialog: () => null }));
vi.mock('@/components/plugins/plugin-iframe-slot', () => import('@/components/plugins/__tests__/fake-slot-offer'));
const device = vi.hoisted(() => ({ isMobile: false }));
vi.mock('@/hooks/use-media-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@/hooks/use-media-query')>()),
useIsMobile: () => device.isMobile,
}));
const folder = {
id: 'f-docs', name: 'Documents', serverName: 'Documents', isDirectory: true, contentType: '', contentLength: 0,
@@ -97,6 +102,7 @@ function rowOf(name: string): HTMLElement {
const dispatch = vi.fn().mockResolvedValue(undefined);
beforeEach(() => {
device.isMobile = false;
localStorage.clear();
dispatch.mockClear();
setActionDispatcher(dispatch);
@@ -159,6 +165,26 @@ describe('file-actions', () => {
expect(context.files.map((f) => f.id)).toEqual(['f-notes', 'f-photo']);
});
it('puts them behind one more button in the phone selection toolbar', async () => {
device.isMobile = true;
act(() => setPluginActions('ocr', 'OCR', 'file-actions', [
{ id: 'archive', label: 'Archive' },
{ id: 'read-text', label: 'Read text', mimeTypes: ['text/'] },
]));
renderBrowser(['notes.txt']);
const toolbar = screen.getByRole('toolbar', { name: 'selection_actions' });
expect(within(toolbar).queryByRole('button', { name: 'Archive' })).toBeNull();
const more = within(toolbar).getByRole('button', { name: 'more_actions' });
// Not in the row that scrolls sideways, where it could end up out of sight.
expect(more.closest('.overflow-x-auto')).toBeNull();
fireEvent.click(more);
expect(await screen.findByRole('menuitem', { name: 'Read text' })).toBeInTheDocument();
await act(async () => { fireEvent.click(screen.getByRole('menuitem', { name: 'Archive' })); });
const context = dispatch.mock.calls[0][3] as { files: FileResourceView[] };
expect(context.files.map((f) => f.id)).toEqual(['f-notes']);
});
it('filters the toolbar by the type of a single selected file', () => {
act(() => setPluginActions('ocr', 'OCR', 'file-actions', [{ id: 'read-text', label: 'Read text', mimeTypes: ['text/'] }]));
renderBrowser(['photo.png']);
+9 -4
View File
@@ -138,8 +138,10 @@ export function FilesHeader({
<ToolButton icon={Scissors} label={t("cut")} onClick={selection.onCut} compact />
<ToolButton icon={Copy} label={t("copy")} onClick={selection.onCopy} compact />
<ToolButton icon={Trash2} label={t("delete")} onClick={selection.onDelete} compact destructive />
<SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} />
{!compact && <SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} />}
</div>
{/* Outside the scrolling row, so the "more" button never scrolls out of sight. */}
{compact && <SelectionPluginActions files={selection.pluginFiles} mimeType={selection.pluginMimeType} overflow />}
<div className="flex-1" />
<DetailsToggle showDetails={showDetails} onToggle={onToggleDetails} label={t("details")} />
</div>
@@ -434,8 +436,11 @@ function SortItem({ label, checked, onClick, icon: Icon }: {
);
}
/** Plugin buttons in the selection toolbar, icon-only like the app's own. */
function SelectionPluginActions({ files, mimeType }: { files: FileResourceView[]; mimeType?: string }) {
/**
* Plugin buttons in the selection toolbar, icon-only like the app's own. On
* phones the toolbar has no room for them: they go behind one "more" button.
*/
function SelectionPluginActions({ files, mimeType, overflow }: { files: FileResourceView[]; mimeType?: string; overflow?: boolean }) {
const context = useMemo(() => ({ files }), [files]);
return <PluginActions surface="file-actions" context={context} mimeType={mimeType} compact className="flex-nowrap gap-0.5" />;
return <PluginActions surface="file-actions" context={context} mimeType={mimeType} compact overflow={overflow} className="flex-nowrap gap-0.5 flex-shrink-0" />;
}