Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions frontend/src/__tests__/unitTests/fileTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, test, expect } from 'vitest';
import { getBrowserRenderableType } from '@/utils/fileTypes';

describe('getBrowserRenderableType', () => {
test('returns "image" for image extensions', () => {
expect(getBrowserRenderableType('photo.png')).toBe('image');
expect(getBrowserRenderableType('photo.jpg')).toBe('image');
expect(getBrowserRenderableType('photo.jpeg')).toBe('image');
expect(getBrowserRenderableType('photo.gif')).toBe('image');
expect(getBrowserRenderableType('photo.webp')).toBe('image');
expect(getBrowserRenderableType('icon.svg')).toBe('image');
expect(getBrowserRenderableType('icon.ico')).toBe('image');
expect(getBrowserRenderableType('photo.bmp')).toBe('image');
});

test('returns "video" for video extensions', () => {
expect(getBrowserRenderableType('clip.mp4')).toBe('video');
expect(getBrowserRenderableType('clip.webm')).toBe('video');
expect(getBrowserRenderableType('clip.ogv')).toBe('video');
});

test('returns "audio" for audio extensions', () => {
expect(getBrowserRenderableType('track.mp3')).toBe('audio');
expect(getBrowserRenderableType('track.wav')).toBe('audio');
expect(getBrowserRenderableType('track.ogg')).toBe('audio');
expect(getBrowserRenderableType('track.aac')).toBe('audio');
expect(getBrowserRenderableType('track.flac')).toBe('audio');
});

test('returns "pdf" for PDF files', () => {
expect(getBrowserRenderableType('doc.pdf')).toBe('pdf');
});

test('returns null for non-renderable extensions', () => {
expect(getBrowserRenderableType('file.txt')).toBeNull();
expect(getBrowserRenderableType('data.csv')).toBeNull();
expect(getBrowserRenderableType('archive.zip')).toBeNull();
expect(getBrowserRenderableType('script.py')).toBeNull();
});

test('is case-insensitive', () => {
expect(getBrowserRenderableType('FILE.PNG')).toBe('image');
expect(getBrowserRenderableType('video.MP4')).toBe('video');
expect(getBrowserRenderableType('Doc.PDF')).toBe('pdf');
});

test('returns null for files without extensions', () => {
expect(getBrowserRenderableType('Makefile')).toBeNull();
expect(getBrowserRenderableType('README')).toBeNull();
});

test('returns null for files ending with a dot', () => {
expect(getBrowserRenderableType('file.')).toBeNull();
});

test('uses the last extension for multi-dot filenames', () => {
expect(getBrowserRenderableType('archive.tar.gz')).toBeNull();
expect(getBrowserRenderableType('image.backup.png')).toBe('image');
});
});
16 changes: 16 additions & 0 deletions frontend/src/components/ui/BrowsePage/FileBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import { useFileBrowserContext } from '@/contexts/FileBrowserContext';
import { usePreferencesContext } from '@/contexts/PreferencesContext';
import useHideDotFiles from '@/hooks/useHideDotFiles';
import { useHandleDownload } from '@/hooks/useHandleDownload';
import { useHandleView } from '@/hooks/useHandleView';
import { detectZarrVersions } from '@/queries/zarrQueries';
import { getBrowserRenderableType } from '@/utils/fileTypes';
import { detectN5, getN5DetectionSignals } from '@/queries/n5Queries';
import { makeMapKey } from '@/utils';
import type { FileOrFolder } from '@/shared.types';
Expand Down Expand Up @@ -61,6 +63,7 @@ export default function FileBrowser({
usePreferencesContext();
const { displayFiles } = useHideDotFiles();
const { handleDownload } = useHandleDownload();
const { handleView } = useHandleView();

const {
contextMenuCoords,
Expand Down Expand Up @@ -138,6 +141,19 @@ export default function FileBrowser({
},
shouldShow: !showPropertiesDrawer
},
{
name: 'View',
action: () => {
const result = handleView();
if (!result.success) {
toast.error(`Error viewing file: ${result.error}`);
}
},
shouldShow:
!propertiesTarget.is_dir &&
!propertiesTarget.is_symlink &&
getBrowserRenderableType(propertiesTarget.name) !== null
},
{
name: 'Download',
action: () => {
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/hooks/useHandleView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useFileBrowserContext } from '@/contexts/FileBrowserContext';
import type { Result } from '@/shared.types';
import { createSuccess, handleError } from '@/utils/errorHandling';
import { getFileURL } from '@/utils/index';

export function useHandleView() {
const { fileBrowserState, fileQuery } = useFileBrowserContext();

const handleView = (): Result<void> => {
if (
!fileQuery.data?.currentFileSharePath ||
!fileBrowserState.propertiesTarget
) {
return handleError(new Error('No file selected for viewing'));
}
try {
const url = getFileURL(
fileQuery.data.currentFileSharePath.name,
fileBrowserState.propertiesTarget.path
);
window.open(url, '_blank', 'noopener,noreferrer');
return createSuccess(undefined);
} catch (error) {
return handleError(error);
}
};

return { handleView };
}
38 changes: 38 additions & 0 deletions frontend/src/utils/fileTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const BROWSER_RENDERABLE_EXTENSIONS: Record<
string,
'image' | 'video' | 'audio' | 'pdf'
> = {
// Images
png: 'image',
jpg: 'image',
jpeg: 'image',
gif: 'image',
webp: 'image',
svg: 'image',
ico: 'image',
bmp: 'image',
// Video
mp4: 'video',
webm: 'video',
ogv: 'video',
// Audio
mp3: 'audio',
wav: 'audio',
ogg: 'audio',
aac: 'audio',
flac: 'audio',
// Documents
pdf: 'pdf'
};

/**
* Returns the browser-renderable media type for a filename, or null if the
* browser cannot render it natively. Used to gate the "View" menu item and
* (in future) to select the appropriate inline preview renderer.
*/
export function getBrowserRenderableType(
filename: string
): 'image' | 'video' | 'audio' | 'pdf' | null {
const ext = filename.toLowerCase().split('.').pop() ?? '';
return BROWSER_RENDERABLE_EXTENSIONS[ext] ?? null;
}
Loading