fix(striker-ui): complete file edit components function-wise

main
Tsu-ba-me 3 years ago
parent b2b347fc72
commit 2e5dd3523b
  1. 168
      striker-ui/components/Files/FileEditForm.tsx
  2. 103
      striker-ui/components/Files/FileInfo.tsx
  3. 90
      striker-ui/components/Files/FileList.tsx
  4. 49
      striker-ui/components/Files/FileUploadInfo.tsx
  5. 69
      striker-ui/components/Files/Files.tsx
  6. 4
      striker-ui/lib/consts/UPLOAD_FILE_TYPES.ts
  7. 3
      striker-ui/types/FileDetailMetadata.d.ts
  8. 4
      striker-ui/types/FileInfoChangeHandler.d.ts
  9. 1
      striker-ui/types/FileInfoMetadata.d.ts
  10. 11
      striker-ui/types/FileInfoProps.d.ts
  11. 7
      striker-ui/types/FileLocation.d.ts
  12. 7
      striker-ui/types/FileOverviewMetadata.d.ts
  13. 1
      striker-ui/types/FileType.d.ts
  14. 1
      striker-ui/types/UploadFileTypes.d.ts

@ -0,0 +1,168 @@
import {
FormEventHandler,
MouseEventHandler,
useEffect,
useState,
} from 'react';
import { Box, Button, Checkbox, checkboxClasses } from '@mui/material';
import { GREY, TEXT } from '../../lib/consts/DEFAULT_THEME';
import { BodyText } from '../Text';
import FileInfo from './FileInfo';
import fetchJSON from '../../lib/fetchers/fetchJSON';
import mainAxiosInstance from '../../lib/singletons/mainAxiosInstance';
type FileEditProps = {
filesOverview: FileOverviewMetadata[];
};
type FileToEdit = FileDetailMetadata & {
dataIncompleteError?: unknown;
isSelected?: boolean;
};
const FileEditForm = ({ filesOverview }: FileEditProps): JSX.Element => {
const [filesToEdit, setFilesToEdit] = useState<FileToEdit[]>([]);
const generateFileInfoChangeHandler = (
fileIndex: number,
): FileInfoChangeHandler => (inputValues, { fileLocationIndex } = {}) => {
if (fileLocationIndex) {
filesToEdit[fileIndex].fileLocations[fileLocationIndex] = {
...filesToEdit[fileIndex].fileLocations[fileLocationIndex],
...inputValues,
};
} else {
filesToEdit[fileIndex] = {
...filesToEdit[fileIndex],
...inputValues,
};
}
};
const editFiles: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
filesToEdit.forEach(({ fileLocations, fileName, fileType, fileUUID }) => {
mainAxiosInstance.put(
`/files/${fileUUID}`,
JSON.stringify({
fileName,
fileType,
fileLocations: fileLocations.map(
({ fileLocationUUID, isFileLocationActive }) => ({
fileLocationUUID,
isFileLocationActive,
}),
),
}),
{
headers: { 'Content-Type': 'application/json' },
},
);
});
};
const purgeFiles: MouseEventHandler<HTMLButtonElement> = () => {
filesToEdit
.filter(({ isSelected }) => isSelected)
.forEach(({ fileUUID }) => {
mainAxiosInstance.delete(`/files/${fileUUID}`);
});
};
useEffect(() => {
Promise.all(
filesOverview.map(async (fileOverview: FileOverviewMetadata) => {
const fileToEdit: FileToEdit = {
...fileOverview,
fileLocations: [],
};
try {
const data = await fetchJSON<string[][]>(
`${process.env.NEXT_PUBLIC_API_URL?.replace(
'/cgi-bin',
'/api',
)}/files/${fileOverview.fileUUID}`,
);
fileToEdit.fileLocations = data.map(
([
,
,
,
,
,
fileLocationUUID,
fileLocationActive,
anvilUUID,
anvilName,
anvilDescription,
]) => ({
anvilDescription,
anvilName,
anvilUUID,
fileLocationUUID,
isFileLocationActive: parseInt(fileLocationActive, 10) === 1,
}),
);
} catch (fetchError) {
fileToEdit.dataIncompleteError = fetchError;
}
return fileToEdit;
}),
).then((fetchedFilesDetail) => setFilesToEdit(fetchedFilesDetail));
}, [filesOverview]);
return (
<form onSubmit={editFiles}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{filesToEdit.map(
({ fileName, fileLocations, fileType, fileUUID }, fileIndex) => (
<Box
key={`file-edit-${fileUUID}`}
sx={{
display: 'flex',
flexDirection: 'row',
}}
>
<Checkbox
onChange={({ target: { checked } }) => {
filesToEdit[fileIndex].isSelected = checked;
}}
sx={{
color: GREY,
[`&.${checkboxClasses.checked}`]: {
color: TEXT,
},
}}
/>
<FileInfo
fileName={fileName}
fileType={fileType}
fileLocations={fileLocations}
onChange={generateFileInfoChangeHandler(fileIndex)}
/>
</Box>
),
)}
{filesToEdit.length > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Button onClick={purgeFiles} sx={{ textTransform: 'none' }}>
<BodyText text="Purge" />
</Button>
<Button sx={{ textTransform: 'none' }} type="submit">
<BodyText text="Update" />
</Button>
</Box>
)}
</Box>
</form>
);
};
export default FileEditForm;

@ -1,38 +1,81 @@
import { import {
Checkbox, Checkbox,
checkboxClasses,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
inputClasses,
MenuItem, MenuItem,
outlinedInputClasses,
Select, Select,
styled,
TextField, TextField,
} from '@mui/material'; } from '@mui/material';
import {
Sync as SyncIcon,
SyncDisabled as SyncDisabledIcon,
} from '@mui/icons-material';
import { TEXT } from '../../lib/consts/DEFAULT_THEME'; import { BLUE, GREY, RED, TEXT } from '../../lib/consts/DEFAULT_THEME';
import { UPLOAD_FILE_TYPES_ARRAY } from '../../lib/consts/UPLOAD_FILE_TYPES'; import { UPLOAD_FILE_TYPES_ARRAY } from '../../lib/consts/UPLOAD_FILE_TYPES';
const FileInfo = ({ type FileInfoProps = Pick<
fileName, FileDetailMetadata,
fileType, 'fileName' | 'fileLocations' | 'fileType'
fileSyncAnvils, > & {
onChange, isReadonly?: boolean;
}: FileInfoProps): JSX.Element => { onChange?: FileInfoChangeHandler;
};
const FILE_INFO_DEFAULT_PROPS: Partial<FileInfoProps> = {
isReadonly: undefined,
onChange: undefined,
};
const StyledTextField = styled(TextField)({
[`& .${outlinedInputClasses.root}`]: {
color: GREY,
},
[`& .${inputClasses.focused}`]: {
color: TEXT,
},
});
const FileLocationActiveCheckbox = styled(Checkbox)({
color: RED,
[`&.${checkboxClasses.checked}`]: {
color: BLUE,
},
});
const FileInfo = (
{
fileName,
fileType,
fileLocations,
isReadonly,
onChange,
}: FileInfoProps = FILE_INFO_DEFAULT_PROPS as FileInfoProps,
): JSX.Element => {
return ( return (
<FormControl> <FormControl>
<TextField <StyledTextField
defaultValue={fileName} defaultValue={fileName}
disabled={isReadonly}
id="file-name" id="file-name"
label="File name" label="File name"
onChange={({ target: { value } }) => onChange={({ target: { value } }) =>
onChange?.call(null, { fileName: value }) onChange?.call(null, { fileName: value })
} }
sx={{ color: TEXT }}
/> />
<Select <Select
defaultValue={fileType} defaultValue={fileType}
disabled={isReadonly}
id="file-type" id="file-type"
label="File type" label="File type"
onChange={({ target: { value } }) => onChange={({ target: { value } }) =>
onChange?.call(null, { fileType: value as UploadFileType }) onChange?.call(null, { fileType: value as FileType })
} }
sx={{ color: TEXT }} sx={{ color: TEXT }}
> >
@ -46,20 +89,38 @@ const FileInfo = ({
}, },
)} )}
</Select> </Select>
{fileSyncAnvils.map( {fileLocations.map(
({ anvilName, anvilDescription, anvilUUID, isSync }) => { (
return ( { anvilName, anvilDescription, anvilUUID, isFileLocationActive },
<FormControlLabel fileLocationIndex,
control={<Checkbox checked={isSync} />} ) => (
key={anvilUUID} <FormControlLabel
label={`${anvilName}: ${anvilDescription}`} control={
value={`${anvilUUID}-sync`} <FileLocationActiveCheckbox
/> checkedIcon={<SyncIcon />}
); defaultChecked={isFileLocationActive}
}, disabled={isReadonly}
icon={<SyncDisabledIcon />}
onChange={({ target: { checked } }) =>
onChange?.call(
null,
{ isFileLocationActive: checked },
{ fileLocationIndex },
)
}
/>
}
key={anvilUUID}
label={`${anvilName}: ${anvilDescription}`}
sx={{ color: TEXT }}
value={`${anvilUUID}-sync`}
/>
),
)} )}
</FormControl> </FormControl>
); );
}; };
FileInfo.defaultProps = FILE_INFO_DEFAULT_PROPS;
export default FileInfo; export default FileInfo;

@ -5,61 +5,63 @@ import { DIVIDER } from '../../lib/consts/DEFAULT_THEME';
import { BodyText } from '../Text'; import { BodyText } from '../Text';
const FileList = ({ list = [] }: { list: string[][] }): JSX.Element => { type FileListProps = {
filesOverview: FileOverviewMetadata[];
};
const FileList = ({ filesOverview }: FileListProps): JSX.Element => {
return ( return (
<List> <List>
{list.map((file) => { {filesOverview.map(
const fileUUID: string = file[0]; ({ fileChecksum, fileName, fileSizeInBytes, fileType, fileUUID }) => {
const fileName: string = file[1]; const fileSize: string = prettyBytes.default(fileSizeInBytes, {
const fileSize: string = prettyBytes.default(parseInt(file[2], 10), { binary: true,
binary: true, });
});
const fileType: string = file[3];
const fileChecksum: string = file[4];
return ( return (
<ListItem button key={fileUUID}> <ListItem button key={fileUUID}>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
width: '100%', width: '100%',
}} }}
> >
<Box sx={{ p: 1, flexGrow: 1 }}> <Box sx={{ p: 1, flexGrow: 1 }}>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
}}
>
<BodyText text={fileName} />
<Divider
flexItem
orientation="vertical"
sx={{
backgroundColor: DIVIDER,
marginLeft: '.5em',
marginRight: '.5em',
}}
/>
<BodyText text={fileType} />
</Box>
<BodyText text={fileSize} />
</Box>
<Box <Box
sx={{ sx={{
alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
}} }}
> >
<BodyText text={fileName} /> <BodyText text={fileChecksum} />
<Divider
flexItem
orientation="vertical"
sx={{
backgroundColor: DIVIDER,
marginLeft: '.5em',
marginRight: '.5em',
}}
/>
<BodyText text={fileType} />
</Box> </Box>
<BodyText text={fileSize} />
</Box>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
}}
>
<BodyText text={fileChecksum} />
</Box> </Box>
</Box> </ListItem>
</ListItem> );
); },
})} )}
</List> </List>
); );
}; };

@ -19,33 +19,35 @@ type FileUploadInfoProps = {
openFilePickerEventEmitter?: EventEmitter; openFilePickerEventEmitter?: EventEmitter;
}; };
type SelectedFile = { type SelectedFile = Pick<
FileDetailMetadata,
'fileName' | 'fileLocations' | 'fileType'
> & {
file: File; file: File;
metadata: FileInfoMetadata;
}; };
type InUploadFile = Pick<FileInfoMetadata, 'fileName'> & { type InUploadFile = Pick<FileDetailMetadata, 'fileName'> & {
progressValue: number; progressValue: number;
}; };
const FILE_UPLOAD_INFO_DEFAULT_PROPS = { const FILE_UPLOAD_INFO_DEFAULT_PROPS: Partial<FileUploadInfoProps> = {
openFilePickerEventEmitter: undefined, openFilePickerEventEmitter: undefined,
}; };
const FileUploadInfo = ({ const FileUploadInfo = (
openFilePickerEventEmitter, {
}: FileUploadInfoProps = FILE_UPLOAD_INFO_DEFAULT_PROPS): JSX.Element => { openFilePickerEventEmitter,
}: FileUploadInfoProps = FILE_UPLOAD_INFO_DEFAULT_PROPS as FileUploadInfoProps,
): JSX.Element => {
const selectFileRef = useRef<HTMLInputElement>(); const selectFileRef = useRef<HTMLInputElement>();
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]); const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
const [inUploadFiles, setInUploadFiles] = useState<InUploadFile[]>([]); const [inUploadFiles, setInUploadFiles] = useState<InUploadFile[]>([]);
const convertMIMETypeToFileTypeKey = ( const convertMIMETypeToFileTypeKey = (fileMIMEType: string): FileType => {
fileMIMEType: string,
): UploadFileType => {
const fileTypesIterator = UPLOAD_FILE_TYPES.entries(); const fileTypesIterator = UPLOAD_FILE_TYPES.entries();
let fileType: UploadFileType | undefined; let fileType: FileType | undefined;
do { do {
const fileTypesResult = fileTypesIterator.next(); const fileTypesResult = fileTypesIterator.next();
@ -72,11 +74,9 @@ const FileUploadInfo = ({
Array.from(files).map( Array.from(files).map(
(file): SelectedFile => ({ (file): SelectedFile => ({
file, file,
metadata: { fileName: file.name,
fileName: file.name, fileLocations: [],
fileType: convertMIMETypeToFileTypeKey(file.type), fileType: convertMIMETypeToFileTypeKey(file.type),
fileSyncAnvils: [],
},
}), }),
), ),
); );
@ -85,9 +85,9 @@ const FileUploadInfo = ({
const generateFileInfoOnChangeHandler = ( const generateFileInfoOnChangeHandler = (
fileIndex: number, fileIndex: number,
): ((inputValues: Partial<FileInfoMetadata>) => void) => (inputValues) => { ): FileInfoChangeHandler => (inputValues) => {
selectedFiles[fileIndex].metadata = { selectedFiles[fileIndex] = {
...selectedFiles[fileIndex].metadata, ...selectedFiles[fileIndex],
...inputValues, ...inputValues,
}; };
}; };
@ -99,10 +99,7 @@ const FileUploadInfo = ({
const selectedFile = selectedFiles.shift(); const selectedFile = selectedFiles.shift();
if (selectedFile) { if (selectedFile) {
const { const { file, fileName, fileType } = selectedFile;
file,
metadata: { fileName, fileType },
} = selectedFile;
const fileFormData = new FormData(); const fileFormData = new FormData();
@ -164,12 +161,14 @@ const FileUploadInfo = ({
( (
{ {
file: { name: originalFileName }, file: { name: originalFileName },
metadata: { fileName, fileType, fileSyncAnvils }, fileName,
fileType,
fileLocations,
}, },
fileIndex, fileIndex,
) => ( ) => (
<FileInfo <FileInfo
{...{ fileName, fileType, fileSyncAnvils }} {...{ fileName, fileType, fileLocations }}
// Use a non-changing key to prevent recreating the component. // Use a non-changing key to prevent recreating the component.
// fileName holds the string from the file-name input, thus it changes when users makes a change. // fileName holds the string from the file-name input, thus it changes when users makes a change.
key={`selected-${originalFileName}`} key={`selected-${originalFileName}`}

@ -1,5 +1,6 @@
import { useState } from 'react';
import { Box, IconButton } from '@mui/material'; import { Box, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add'; import { Add as AddIcon, Edit as EditIcon } from '@mui/icons-material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import EventEmitter from 'events'; import EventEmitter from 'events';
@ -11,6 +12,7 @@ import Spinner from '../Spinner';
import { HeaderText } from '../Text'; import { HeaderText } from '../Text';
import FileList from './FileList'; import FileList from './FileList';
import FileUploadInfo from './FileUploadInfo'; import FileUploadInfo from './FileUploadInfo';
import FileEditForm from './FileEditForm';
const PREFIX = 'Files'; const PREFIX = 'Files';
@ -27,18 +29,59 @@ const StyledDiv = styled('div')(() => ({
}, },
})); }));
const StyledIconButton = styled(IconButton)(ICON_BUTTON_STYLE);
const Files = (): JSX.Element => { const Files = (): JSX.Element => {
const openFilePickerEventEmitter: EventEmitter = new EventEmitter(); const [isEditMode, setIsEditMode] = useState<boolean>(false);
const { data: fileList, isLoading } = PeriodicFetch( const openFilePickerEventEmitter: EventEmitter = new EventEmitter();
`${process.env.NEXT_PUBLIC_API_URL?.replace('/cgi-bin', '/api')}/files`,
0,
);
const onAddFileButtonClick = () => { const onAddFileButtonClick = () => {
openFilePickerEventEmitter.emit('open'); openFilePickerEventEmitter.emit('open');
}; };
const onEditFileButtonClick = () => {
setIsEditMode(!isEditMode);
};
const buildFileList = (
rawFilesOverview: string[][] = [],
isLoadingRawFilesOverview: boolean,
): JSX.Element => {
let elements: JSX.Element;
if (isLoadingRawFilesOverview) {
elements = <Spinner />;
} else {
const filesOverview: FileOverviewMetadata[] = rawFilesOverview.map(
([fileUUID, fileName, fileSizeInBytes, fileType, fileChecksum]) => {
return {
fileChecksum,
fileName,
fileSizeInBytes: parseInt(fileSizeInBytes, 10),
fileType: fileType as FileType,
fileUUID,
};
},
);
elements = isEditMode ? (
<FileEditForm filesOverview={filesOverview} />
) : (
<FileList filesOverview={filesOverview} />
);
}
return elements;
};
const {
data: rawFilesOverview,
isLoading: isLoadingRawFilesOverview,
} = PeriodicFetch<string[][]>(
`${process.env.NEXT_PUBLIC_API_URL?.replace('/cgi-bin', '/api')}/files`,
0,
);
return ( return (
<Panel> <Panel>
<StyledDiv> <StyledDiv>
@ -47,18 +90,20 @@ const Files = (): JSX.Element => {
<HeaderText text="Files" /> <HeaderText text="Files" />
</Box> </Box>
<Box> <Box>
<IconButton <StyledIconButton onClick={onAddFileButtonClick}>
className={classes.addFileButton}
onClick={onAddFileButtonClick}
>
<AddIcon /> <AddIcon />
</IconButton> </StyledIconButton>
</Box>
<Box>
<StyledIconButton onClick={onEditFileButtonClick}>
<EditIcon />
</StyledIconButton>
</Box> </Box>
</Box> </Box>
<FileUploadInfo <FileUploadInfo
openFilePickerEventEmitter={openFilePickerEventEmitter} openFilePickerEventEmitter={openFilePickerEventEmitter}
/> />
{isLoading ? <Spinner /> : <FileList list={fileList} />} {buildFileList(rawFilesOverview, isLoadingRawFilesOverview)}
</StyledDiv> </StyledDiv>
</Panel> </Panel>
); );

@ -1,11 +1,11 @@
export const UPLOAD_FILE_TYPES_ARRAY: ReadonlyArray< export const UPLOAD_FILE_TYPES_ARRAY: ReadonlyArray<
[UploadFileType, [string, string]] [FileType, [string, string]]
> = [ > = [
['iso', ['application/x-cd-image', 'ISO (optical disc)']], ['iso', ['application/x-cd-image', 'ISO (optical disc)']],
['other', ['text/plain', 'Other file type']], ['other', ['text/plain', 'Other file type']],
['script', ['text/plain', 'Script (program)']], ['script', ['text/plain', 'Script (program)']],
]; ];
export const UPLOAD_FILE_TYPES: ReadonlyMap< export const UPLOAD_FILE_TYPES: ReadonlyMap<
UploadFileType, FileType,
[string, string] [string, string]
> = new Map(UPLOAD_FILE_TYPES_ARRAY); > = new Map(UPLOAD_FILE_TYPES_ARRAY);

@ -0,0 +1,3 @@
type FileDetailMetadata = FileOverviewMetadata & {
fileLocations: FileLocation[];
};

@ -0,0 +1,4 @@
type FileInfoChangeHandler = (
inputValues: Partial<FileDetailMetadata> | Partial<FileLocation>,
options?: { fileLocationIndex?: number },
) => void;

@ -1 +0,0 @@
declare type FileInfoMetadata = Omit<FileInfoProps, 'onChange'>;

@ -1,11 +0,0 @@
declare type FileInfoProps = {
fileName: string;
fileType: UploadFileType;
fileSyncAnvils: Array<{
anvilName: string;
anvilDescription: string;
anvilUUID: string;
isSync: boolean;
}>;
onChange?: (inputValues: Partial<FileInfoMetadata>) => void;
};

@ -0,0 +1,7 @@
type FileLocation = {
anvilName: string;
anvilDescription: string;
anvilUUID: string;
fileLocationUUID: string;
isFileLocationActive: boolean;
};

@ -0,0 +1,7 @@
type FileOverviewMetadata = {
fileChecksum: string;
fileName: string;
fileSizeInBytes: number;
fileType: FileType;
fileUUID: string;
};

@ -0,0 +1 @@
declare type FileType = 'iso' | 'other' | 'script';

@ -1 +0,0 @@
declare type UploadFileType = 'iso' | 'other' | 'script';
Loading…
Cancel
Save