V2 Make FileId type opaque and use consistently throughout project (#4307)

# Description of Changes
The `FileId` type in V2 currently is just defined to be a string. This
makes it really easy to accidentally pass strings into things accepting
file IDs (such as file names). This PR makes the `FileId` type [an
opaque
type](https://www.geeksforgeeks.org/typescript/opaque-types-in-typescript/),
so it is compatible with things accepting strings (arguably not ideal
for this...) but strings are not compatible with it without explicit
conversion.

The PR also includes changes to use `FileId` consistently throughout the
project (everywhere I could find uses of `fileId: string`), so that we
have the maximum benefit from the type safety.

> [!note]
> I've marked quite a few things as `FIX ME` where we're passing names
in as IDs. If that is intended behaviour, I'm happy to remove the fix me
and insert a cast instead, but they probably need comments explaining
why we're using a file name as an ID.
This commit is contained in:
James Brunton
2025-08-28 10:56:07 +01:00
committed by GitHub
parent 581bafbd37
commit e142af2863
32 changed files with 600 additions and 574 deletions

View File

@@ -5,6 +5,7 @@ import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard";
import { FileRecord } from "../../types/fileContext";
import { FileId } from "../../types/file";
interface FileGridProps {
files: Array<{ file: File; record?: FileRecord }>;
@@ -12,8 +13,8 @@ interface FileGridProps {
onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
onView?: (item: { file: File; record?: FileRecord }) => void;
onEdit?: (item: { file: File; record?: FileRecord }) => void;
onSelect?: (fileId: string) => void;
selectedFiles?: string[];
onSelect?: (fileId: FileId) => void;
selectedFiles?: FileId[];
showSearch?: boolean;
showSort?: boolean;
maxDisplay?: number; // If set, shows only this many files with "Show All" option
@@ -119,11 +120,11 @@ const FileGrid = ({
direction="row"
wrap="wrap"
gap="md"
h="30rem"
h="30rem"
style={{ overflowY: "auto", width: "100%" }}
>
{displayFiles.map((item, idx) => {
const fileId = item.record?.id || item.file.name;
const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */;
const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return (

View File

@@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
Text,
Button,
Group,
Stack,
Checkbox,
ScrollArea,
import {
Modal,
Text,
Button,
Group,
Stack,
Checkbox,
ScrollArea,
Box,
Image,
Badge,
@@ -15,6 +15,7 @@ import {
} from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next';
import { FileId } from '../../types/file';
interface FilePickerModalProps {
opened: boolean;
@@ -30,7 +31,7 @@ const FilePickerModal = ({
onSelectFiles,
}: FilePickerModalProps) => {
const { t } = useTranslation();
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
// Reset selection when modal opens
useEffect(() => {
@@ -39,9 +40,9 @@ const FilePickerModal = ({
}
}, [opened]);
const toggleFileSelection = (fileId: string) => {
const toggleFileSelection = (fileId: FileId) => {
setSelectedFileIds(prev => {
return prev.includes(fileId)
return prev.includes(fileId)
? prev.filter(id => id !== fileId)
: [...prev, fileId];
});
@@ -56,10 +57,10 @@ const FilePickerModal = ({
};
const handleConfirm = async () => {
const selectedFiles = storedFiles.filter(f =>
const selectedFiles = storedFiles.filter(f =>
selectedFileIds.includes(f.id)
);
// Convert stored files to File objects
const convertedFiles = await Promise.all(
selectedFiles.map(async (fileItem) => {
@@ -68,12 +69,12 @@ const FilePickerModal = ({
if (fileItem instanceof File) {
return fileItem;
}
// If it has a file property, use that
if (fileItem.file && fileItem.file instanceof File) {
return fileItem.file;
}
// If it's from IndexedDB storage, reconstruct the File
if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') {
const arrayBuffer = await fileItem.arrayBuffer();
@@ -83,8 +84,8 @@ const FilePickerModal = ({
lastModified: fileItem.lastModified || Date.now()
});
}
// If it has data property, reconstruct the File
// If it has data property, reconstruct the File
if (fileItem.data) {
const blob = new Blob([fileItem.data], { type: fileItem.type || 'application/pdf' });
return new File([blob], fileItem.name, {
@@ -92,7 +93,7 @@ const FilePickerModal = ({
lastModified: fileItem.lastModified || Date.now()
});
}
console.warn('Could not convert file item:', fileItem);
return null;
} catch (error) {
@@ -101,10 +102,10 @@ const FilePickerModal = ({
}
})
);
// Filter out any null values and return valid Files
const validFiles = convertedFiles.filter((f): f is File => f !== null);
onSelectFiles(validFiles);
onClose();
};
@@ -156,18 +157,18 @@ const FilePickerModal = ({
{storedFiles.map((file) => {
const fileId = file.id;
const isSelected = selectedFileIds.includes(fileId);
return (
<Box
key={fileId}
p="sm"
style={{
border: isSelected
? '2px solid var(--mantine-color-blue-6)'
border: isSelected
? '2px solid var(--mantine-color-blue-6)'
: '1px solid var(--mantine-color-gray-3)',
borderRadius: 8,
backgroundColor: isSelected
? 'var(--mantine-color-blue-0)'
backgroundColor: isSelected
? 'var(--mantine-color-blue-0)'
: 'transparent',
cursor: 'pointer',
transition: 'all 0.2s ease'
@@ -180,7 +181,7 @@ const FilePickerModal = ({
onChange={() => toggleFileSelection(fileId)}
onClick={(e) => e.stopPropagation()}
/>
{/* Thumbnail */}
<Box
style={{
@@ -246,11 +247,11 @@ const FilePickerModal = ({
<Button variant="light" onClick={onClose}>
{t("close", "Cancel")}
</Button>
<Button
<Button
onClick={handleConfirm}
disabled={selectedFileIds.length === 0}
>
{selectedFileIds.length > 0
{selectedFileIds.length > 0
? `${t("fileUpload.loadFromStorage", "Load")} ${selectedFileIds.length} ${t("fileUpload.uploadFiles", "Files")}`
: t("fileUpload.loadFromStorage", "Load Files")
}
@@ -261,4 +262,4 @@ const FilePickerModal = ({
);
};
export default FilePickerModal;
export default FilePickerModal;