* refactor: add typescript dependencies
* chore: update chakra-ui
* refactor: typescript config
* refactor: convert <MenuBar> component to typescript
* refactor: convert <TooltipActionBtn> component to typescript
* refactor: improve UX in file upload
* refactor: upgrade dependencies
* refactor: prepare data provider
* refactor: prevent importing bad fields
* refactor: extract merge to provider
* refactor(upload): parser merges only given fields
* refactor(upload): add event fields to excel
* refactor(upload): improve styling on modal open
* style: improve styling in menu
* style: prevent global pollution
* feat(upload): add upload options
* fix: avoid potential bug in log queue
This commit is contained in:
Carlos Valente
2022-09-17 23:28:52 +02:00
committed by GitHub
parent 94b03312a6
commit 3128f5a195
25 changed files with 788 additions and 368 deletions
@@ -1,27 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
export default function TooltipActionBtn(props) {
const { clickHandler, icon, color, size='xs', tooltip, openDelay = 0, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
{...rest}
/>
</Tooltip>
);
}
TooltipActionBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
color: PropTypes.string,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
tooltip: PropTypes.string,
openDelay: PropTypes.number
}
@@ -0,0 +1,27 @@
import { IconButton } from '@chakra-ui/button';
import { IconButtonProps } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
export type Sizes = 'xs' | 'sm' | 'md' | 'lg';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: () => void;
tooltip: string;
openDelay?: number;
}
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton
{...rest}
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
className={className}
/>
</Tooltip>
);
}
@@ -0,0 +1,45 @@
@use '../../../theme/main' as *;
.modalBody {
min-height: 40vh;
display: flex;
flex-direction: column;
gap: 16px;
.options {
margin-bottom: 1.5em;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.notes {
font-size: 0.9em;
}
.info {
background-color: $bg-gray;
margin: 1em 0;
padding: 0.5em;
border-radius: 2px;
color: black;
position: relative;
}
.corner {
position: absolute;
right: 4px;
top: 4px;
}
.infoList {
font-size: 0.9em;
padding-left: 8px;
}
.flexColumnLeft {
display: flex;
flex-direction: column;
align-items: flex-start;
}
}
@@ -0,0 +1,137 @@
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
import { Button } from '@chakra-ui/button';
import {
Checkbox,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Progress,
} from '@chakra-ui/react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { useQueryClient } from '@tanstack/react-query';
import { EVENTS_TABLE } from '../../api/apiConstants';
import { uploadEvents } from '../../api/ontimeApi';
import { LoggingContext } from '../../context/LoggingContext';
import TooltipActionBtn from '../buttons/TooltipActionBtn';
import { validateFile } from './utils';
import style from './UploadModal.module.scss';
interface UploadModalProps {
onClose: () => void;
isOpen: boolean;
}
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const [errors, setErrors] = useState<string[]>([]);
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState(0);
const overrideOptionRef = useRef<HTMLInputElement>(null);
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileUploaded = event?.target?.files?.[0];
if (!fileUploaded) return;
const validate = validateFile(fileUploaded);
setErrors(validate.errors);
if (validate.isValid) {
setFile(fileUploaded);
} else {
setFile(null);
}
}, []);
const handleUpload = useCallback(async () => {
if (file) {
try {
await uploadEvents(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
} catch (error) {
emitError(`Failed uploading file: ${error}`);
} finally {
await queryClient.invalidateQueries(EVENTS_TABLE);
setFile(null);
}
}
}, [emitError, file, queryClient]);
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>File upload</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<FormControl isInvalid={errors.length > 0}>
<FormLabel>Select file to upload</FormLabel>
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
{errors.length === 0 ? (
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
) : (
<FormErrorMessage className={style.flexColumnLeft}>
{errors.map((error) => (
<span key={error}>{error}</span>
))}
</FormErrorMessage>
)}
</FormControl>
<div className={style.options}>
<b>Options</b>
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
<span className={style.notes}>This will prevent overriding user settings</span>
</div>
{file && (
<div className={style.info}>
<span>File ready to upload</span>
<TooltipActionBtn
clickHandler={() => setFile(null)}
tooltip='Cancel'
aria-label='Cancel'
className={style.corner}
size='sm'
variant='ghosted'
icon={<IoCloseSharp />}
/>
<ul className={style.infoList}>
<li>{file.name}</li>
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
<li>{file.type}</li>
</ul>
</div>
)}
<Progress value={progress} />
</ModalBody>
<ModalFooter>
<Button
colorScheme='blue'
disabled={!file || errors.length > 0}
onClick={handleUpload}
isLoading={progress < 0 && progress >= 100}
>
Upload
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,25 @@
type ValidationStatus = {
errors: string[];
isValid: boolean;
};
export function validateFile(file: File): ValidationStatus {
const status:ValidationStatus = { errors: [], isValid: true };
if (!file) {
status.errors.push('No file to upload');
status.isValid = false;
}
// Limit file size to 1MB
if (file.size > 1000000) {
status.errors.push('File size limit (1MB) exceeded');
status.isValid = false;
}
// Check file extension
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
status.errors.push('Unhandled file type');
status.isValid = false;
}
return status;
}