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 {
Checkbox,
checkboxClasses,
FormControl,
FormControlLabel,
inputClasses,
MenuItem,
outlinedInputClasses,
Select,
styled,
TextField,
} 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';
const FileInfo = ({
fileName,
fileType,
fileSyncAnvils,
onChange,
}: FileInfoProps): JSX.Element => {
type FileInfoProps = Pick<
FileDetailMetadata,
'fileName' | 'fileLocations' | 'fileType'
> & {
isReadonly?: boolean;
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 (
<FormControl>
<TextField
<StyledTextField
defaultValue={fileName}
disabled={isReadonly}
id="file-name"
label="File name"
onChange={({ target: { value } }) =>
onChange?.call(null, { fileName: value })
}
sx={{ color: TEXT }}
/>
<Select
defaultValue={fileType}
disabled={isReadonly}
id="file-type"
label="File type"
onChange={({ target: { value } }) =>
onChange?.call(null, { fileType: value as UploadFileType })
onChange?.call(null, { fileType: value as FileType })
}
sx={{ color: TEXT }}
>
@ -46,20 +89,38 @@ const FileInfo = ({
},
)}
</Select>
{fileSyncAnvils.map(
({ anvilName, anvilDescription, anvilUUID, isSync }) => {
return (
<FormControlLabel
control={<Checkbox checked={isSync} />}
key={anvilUUID}
label={`${anvilName}: ${anvilDescription}`}
value={`${anvilUUID}-sync`}
/>
);
},
{fileLocations.map(
(
{ anvilName, anvilDescription, anvilUUID, isFileLocationActive },
fileLocationIndex,
) => (
<FormControlLabel
control={
<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>
);
};
FileInfo.defaultProps = FILE_INFO_DEFAULT_PROPS;
export default FileInfo;

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

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

@ -1,5 +1,6 @@
import { useState } from 'react';
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 EventEmitter from 'events';
@ -11,6 +12,7 @@ import Spinner from '../Spinner';
import { HeaderText } from '../Text';
import FileList from './FileList';
import FileUploadInfo from './FileUploadInfo';
import FileEditForm from './FileEditForm';
const PREFIX = 'Files';
@ -27,18 +29,59 @@ const StyledDiv = styled('div')(() => ({
},
}));
const StyledIconButton = styled(IconButton)(ICON_BUTTON_STYLE);
const Files = (): JSX.Element => {
const openFilePickerEventEmitter: EventEmitter = new EventEmitter();
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const { data: fileList, isLoading } = PeriodicFetch(
`${process.env.NEXT_PUBLIC_API_URL?.replace('/cgi-bin', '/api')}/files`,
0,
);
const openFilePickerEventEmitter: EventEmitter = new EventEmitter();
const onAddFileButtonClick = () => {
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 (
<Panel>
<StyledDiv>
@ -47,18 +90,20 @@ const Files = (): JSX.Element => {
<HeaderText text="Files" />
</Box>
<Box>
<IconButton
className={classes.addFileButton}
onClick={onAddFileButtonClick}
>
<StyledIconButton onClick={onAddFileButtonClick}>
<AddIcon />
</IconButton>
</StyledIconButton>
</Box>
<Box>
<StyledIconButton onClick={onEditFileButtonClick}>
<EditIcon />
</StyledIconButton>
</Box>
</Box>
<FileUploadInfo
openFilePickerEventEmitter={openFilePickerEventEmitter}
/>
{isLoading ? <Spinner /> : <FileList list={fileList} />}
{buildFileList(rawFilesOverview, isLoadingRawFilesOverview)}
</StyledDiv>
</Panel>
);

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