refactor: migrate project upload (#796)

* refactor: migrate project upload
This commit is contained in:
Carlos Valente
2024-03-01 22:59:00 +01:00
committed by GitHub
parent 8df419d835
commit 5b01329c37
54 changed files with 698 additions and 1134 deletions
+16 -4
View File
@@ -135,6 +135,21 @@ export const downloadRundown = (fileName?: string) => {
);
};
/**
* @description HTTP request to upload project file
* @return {Promise}
*/
export async function importProjectFile(file: File): Promise<void> {
const formData = new FormData();
formData.append('userFile', file);
const response = await axios.post(`${ontimeURL}/db`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
// TODO: should this be extracted to shared code?
export type ProjectFileImportOptions = {
onlyRundown: boolean;
@@ -341,10 +356,7 @@ export async function createProject(
}
>,
): Promise<MessageResponse> {
// TODO: is this URL correct?
const url = `${ontimeURL}/project`;
const decodedUrl = decodeURIComponent(url);
const res = await axios.post(decodedUrl, project);
const res = await axios.post(`${ontimeURL}/project`, project);
return res.data;
}
@@ -38,37 +38,6 @@ export function validateProjectFile(file: File) {
}
}
/**
* Validates a file according to the app upload contract
* @throws
* @param file
*/
export function validateFile(file: File) {
if (!file) {
throw new Error('No file to upload');
}
// Check if file is empty
if (file.size === 0) {
throw new Error('File is empty');
}
// Limit file size of a project file to around 1MB
if (file.name.endsWith('.json') && file.size > 1_000_000) {
throw new Error('File size limit (1MB) exceeded');
}
// Limit file size of an excel file to around 10MB
if (file.name.endsWith('.xlsx') && file.size > 10_000_000) {
throw new Error('File size limit (10MB) exceeded');
}
// Check file extension
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
throw new Error('Unhandled file type');
}
}
export function isExcelFile(file: File | null) {
return file?.name.endsWith('.xlsx');
}
@@ -1,10 +1,18 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Textarea } from '@chakra-ui/react';
import { maybeAxiosError } from '../../../../common/api/apiUtils';
import { createProject } from '../../../../common/api/ontimeApi';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss';
export type ProjectCreateFormValues = {
interface ProjectCreateFromProps {
onClose: () => void;
}
type ProjectCreateFormValues = {
title?: string;
description?: string;
publicInfo?: string;
@@ -13,13 +21,11 @@ export type ProjectCreateFormValues = {
backstageUrl?: string;
};
interface ProjectCreateFormProps {
onCancel: () => void;
onSubmit: (values: ProjectCreateFormValues) => Promise<void>;
submitError: string | null;
}
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
const { onClose } = props;
const [error, setError] = useState<string | null>(null);
export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: ProjectCreateFormProps) {
const {
handleSubmit,
register,
@@ -33,12 +39,40 @@ export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: P
},
});
// set focus to first field
useEffect(() => {
setFocus('title');
}, [setFocus]);
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
try {
setError(null);
const filename = values.title?.trim();
await createProject({
...values,
filename,
});
onClose();
} catch (error) {
setError(maybeAxiosError(error));
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Panel.Section as='form' onSubmit={handleSubmit(handleSubmitCreate)}>
<Panel.Title>
Create new project
<div className={style.createActionButtons}>
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
Cancel
</Button>
<Button isDisabled={!isValid} type='submit' isLoading={isSubmitting} variant='ontime-filled' size='sm'>
Create
</Button>
</div>
</Panel.Title>
{error && <Panel.Error>{error}</Panel.Error>}
<div className={style.createFormInputField}>
<label>
Project title
@@ -115,22 +149,6 @@ export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: P
/>
</label>
</div>
<div className={style.createActionButtons}>
<Button onClick={onCancel} variant='ontime-ghosted' size='sm'>
Cancel
</Button>
<Button
isDisabled={!isValid}
type='submit'
isLoading={isSubmitting}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
Create
</Button>
</div>
{submitError && <span className={style.error}>{submitError}</span>}
</form>
</Panel.Section>
);
}
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@chakra-ui/react';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss';
export type ProjectFormValues = {
@@ -62,7 +64,7 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel, subm
</Button>
</div>
</form>
{submitError && <span className={style.error}>{submitError}</span>}
{submitError && <Panel.Error>{submitError}</Panel.Error>}
</>
);
}
@@ -1,48 +1,18 @@
import { useMemo, useState } from 'react';
import { maybeAxiosError } from '../../../../common/api/apiUtils';
import { createProject } from '../../../../common/api/ontimeApi';
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
import * as Panel from '../PanelUtils';
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
import ProjectListItem, { EditMode } from './ProjectListItem';
import style from './ProjectPanel.module.scss';
interface ProjectListProps {
isCreatingProject: boolean;
onToggleCreate: () => void;
}
export default function ProjectList({ isCreatingProject, onToggleCreate }: ProjectListProps) {
export default function ProjectList() {
const { data, refetch } = useProjectList();
const { files, lastLoadedProject } = data;
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
const [editingFilename, setEditingFilename] = useState<string | null>(null);
const [submitError, setSubmitError] = useState<string | null>(null);
const handleToggleCreateMode = () => {
onToggleCreate();
setSubmitError(null);
};
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
try {
setSubmitError(null);
const filename = values.title?.trim();
await createProject({
...values,
filename,
});
await refetch();
handleToggleCreateMode();
} catch (error) {
setSubmitError(maybeAxiosError(error));
}
};
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
@@ -79,14 +49,6 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
</tr>
</thead>
<tbody>
{isCreatingProject ? (
<tr className={style.createContainer}>
<td colSpan={99}>
<ProjectCreateForm onSubmit={handleSubmitCreate} onCancel={handleToggleCreateMode} submitError='' />
{submitError && <span className={style.createSubmitError}>{submitError}</span>}
</td>
</tr>
) : null}
{reorderedProjectFiles.map((project) => (
<ProjectListItem
key={project.filename}
@@ -85,9 +85,10 @@ export default function ProjectListItem({
};
const isCurrentlyBeingEdited = editingMode && filename === editingFilename;
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
return (
<tr key={filename} className={current ? style.current : undefined}>
<tr key={filename} className={classes}>
{isCurrentlyBeingEdited ? (
<td colSpan={99}>
<ProjectForm
@@ -160,6 +161,7 @@ function ActionMenu({
as={IconButton}
aria-label='Options'
icon={<IoEllipsisHorizontal />}
color='#e2e2e2' // $gray-200
variant='ontime-ghosted'
size='sm'
/>
@@ -1,6 +1,5 @@
.current {
color: $blue-400;
background-color: $gray-1350;
background-color: $blue-900;
}
.actionButton {
@@ -23,25 +22,23 @@
padding-bottom: 0.5rem;
}
.actionButtons {
.headerButtons,
.actionButtons,
.createActionButtons {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 1rem;
}
.actionButtons {
margin-left: 1rem;
}
.createActionButtons {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: 1rem;
justify-content: flex-end;
}
.error {
color: $red-500;
}
.createContainer {
padding: 0.5rem 0;
}
@@ -50,12 +47,6 @@
text-transform: capitalize;
}
.createSubmitError {
font-size: calc(1rem - 2px);
font-weight: 400;
color: $red-500;
}
.containCell {
max-width: 400px;
}
@@ -1,34 +1,92 @@
import { useState } from 'react';
import { Button } from '@chakra-ui/react';
import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
import { importProjectFile } from '../../../../common/api/ontimeApi';
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
import * as Panel from '../PanelUtils';
import ProjectCreateForm from './ProjectCreateForm';
import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss';
export default function ProjectPanel() {
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev);
};
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target?.files?.[0];
if (!selectedFile) {
return;
}
setLoading('import');
try {
validateProjectFile(selectedFile);
await importProjectFile(selectedFile);
} catch (error) {
const errorMessage = maybeAxiosError(error);
setError(`Error uploading file: ${errorMessage}`);
} finally {
invalidateAllCaches();
}
setLoading(null);
};
return (
<>
<Panel.Header>Project</Panel.Header>
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleImport}
accept='.json'
data-testid='file-input'
/>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>
Manage projects
<div style={{ display: 'flex', gap: '1rem' }}>
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
<div className={style.headerButtons}>
<Button
variant='ontime-subtle'
onClick={handleSelectFile}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
isLoading={loading === 'import'}
>
Import
</Button>
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
<Button
variant='ontime-subtle'
onClick={handleToggleCreate}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
rightIcon={<IoAdd />}
>
Add
</Button>
</div>
</Panel.SubHeader>
<ProjectList onToggleCreate={handleToggleCreate} isCreatingProject={isCreatingProject} />
{error && <Panel.Error>{error}</Panel.Error>}
{isCreatingProject && <ProjectCreateForm onClose={() => setIsCreatingProject(false)} />}
<ProjectList />
</Panel.Card>
</Panel.Section>
</>
@@ -98,6 +98,7 @@ export default function SourcesPanel() {
}
}
};
const handleFinished = () => {
setImportFlow('none');
setRundown(null);
@@ -119,7 +120,6 @@ export default function SourcesPanel() {
const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile);
const showReview = rundown !== null && customFields !== null;
console.log(isAuthenticated);
return (
<>
<Panel.Header>Data sources</Panel.Header>
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
import AppSettings from '../app-settings/AppSettings';
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
import MenuBar from '../menu/MenuBar';
import UploadModal from '../modals/upload-modal/UploadModal';
import Overview from '../overview/Overview';
import styles from './Editor.module.scss';
@@ -24,7 +23,6 @@ export default function Editor() {
};
const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
// Set window title
useEffect(() => {
@@ -36,7 +34,6 @@ export default function Editor() {
return (
<>
<ErrorBoundary>
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
</ErrorBoundary>
<div className={styles.mainContainer} data-testid='event-editor'>
@@ -45,8 +42,6 @@ export default function Editor() {
isOldSettingsOpen={isOldSettingsOpen}
onSettingsOpen={onSettingsOpen}
onSettingsClose={onSettingsClose}
isUploadOpen={isUploadModalOpen}
onUploadOpen={onUploadModalOpen}
openSettings={handleSettings}
isSettingsOpen={isSettingsOpen}
/>
+5 -26
View File
@@ -3,7 +3,6 @@ import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { IoSnowOutline } from '@react-icons/all-files/io5/IoSnowOutline';
@@ -21,8 +20,6 @@ interface MenuBarProps {
isOldSettingsOpen: boolean;
onSettingsOpen: () => void;
onSettingsClose: () => void;
isUploadOpen: boolean;
onUploadOpen: () => void;
openSettings: (newTab?: string) => void;
isSettingsOpen: boolean;
}
@@ -40,15 +37,7 @@ const buttonStyle = {
};
const MenuBar = (props: MenuBarProps) => {
const {
isOldSettingsOpen,
onSettingsOpen,
onSettingsClose,
isUploadOpen,
onUploadOpen,
openSettings,
isSettingsOpen,
} = props;
const { isOldSettingsOpen, onSettingsOpen, onSettingsClose, openSettings, isSettingsOpen } = props;
const { isElectron, sendToElectron } = useElectronEvent();
const appMode = useAppMode((state) => state.mode);
@@ -95,16 +84,6 @@ const MenuBar = (props: MenuBarProps) => {
<div className={style.menu}>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
isDisabled={appMode === AppMode.Run}
icon={<IoPushOutline />}
className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen}
tooltip='Import project file'
aria-label='Import project file'
/>
<div className={style.gap} />
<RundownMenu>
<Tooltip label='Rundown...'>
<MenuButton as={IconButton} icon={<IoAdd />} {...buttonStyle} aria-label='Rundown menu' />
@@ -141,8 +120,8 @@ const MenuBar = (props: MenuBarProps) => {
icon={<IoSettingsOutline />}
className={isOldSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
tooltip='Settings deprecated'
aria-label='Settings deprecated'
/>
<TooltipActionBtn
@@ -150,8 +129,8 @@ const MenuBar = (props: MenuBarProps) => {
className={cx([isSettingsOpen ? style.open : null, style.bottom])}
icon={<IoSettingsOutline />}
clickHandler={() => openSettings()}
tooltip='About'
aria-label='About'
tooltip='Application settings'
aria-label='Application settings'
/>
</div>
);
@@ -1,67 +0,0 @@
import { ChangeEvent, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import { validateFile } from '../../../common/utils/uploadUtils';
import UploadEntry from './upload-entry/UploadEntry';
import { useUploadModalContextStore } from './uploadModalContext';
import style from './UploadModal.module.scss';
export default function UploadFile() {
const fileInputRef = useRef<HTMLInputElement>(null);
const { file, setFile, progress } = useUploadModalContextStore();
const [errors, setErrors] = useState<string>('');
const clearFile = () => {
setFile(null);
setErrors('');
};
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
setErrors('');
const selectedFile = event?.target?.files?.[0];
if (!selectedFile) {
setFile(null);
return;
}
try {
validateFile(selectedFile);
setFile(selectedFile);
} catch (error) {
if (error instanceof Error) {
setErrors(error.message);
} else {
setErrors('An unexpected error occurred while validating the file.');
}
setFile(null);
}
};
const handleClick = () => {
fileInputRef.current?.click();
};
return (
<>
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleFile}
accept='.json'
data-testid='file-input'
/>
{!file && (
<div className={style.uploadArea} onClick={handleClick} role='button'>
Click to select Ontime project
</div>
)}
{(file || errors) && <UploadEntry file={file} errors={errors} progress={progress} handleClear={clearFile} />}
</>
);
}
@@ -1,47 +0,0 @@
@import "../Modal.module.scss";
.uploadBody {
display: flex;
flex-direction: column;
gap: 1rem;
}
.uploadArea {
margin: 0 auto;
width: 100%;
max-width: 550px;
min-height: 150px;
border: 2px dashed $gray-200;
border-radius: 3px;
display: grid;
place-content: center;
transition-property: background-color;
transition-duration: $transition-time-action;
font-size: calc(1rem - 1px);
&:hover {
border: 2px solid $blue-500;
background-color: $blue-50;
cursor: pointer;
}
&.comment {
color: $modal-note-color;
}
}
.uploadOptions {
display: flex;
flex-direction: column;
}
.pad {
margin: 8px;
}
.twoColumn {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
@@ -1,161 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { OntimeRundown } from 'ontime-types';
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
import { ProjectFileImportOptions, uploadProjectFile } from '../../../common/api/ontimeApi';
import { isOntimeFile } from '../../../common/utils/uploadUtils';
import OntimeFileOptions from './upload-options/OntimeFileOptions';
import UploadFile from './UploadFile';
import { useUploadModalContextStore } from './uploadModalContext';
import style from './UploadModal.module.scss';
export type UploadStep = 'import' | 'review';
interface UploadModalProps {
onClose: () => void;
isOpen: boolean;
}
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const { file, setProgress, clear } = useUploadModalContextStore();
const [uploadStep, setUploadStep] = useState<UploadStep>('import');
const [submitting, setSubmitting] = useState(false);
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
const [errors, setErrors] = useState('');
const ontimeFileOptions = useRef<Partial<ProjectFileImportOptions>>({});
const updateOntimeFileOptions = <T extends keyof ProjectFileImportOptions>(
field: T,
value: ProjectFileImportOptions[T],
) => {
ontimeFileOptions.current = { ...ontimeFileOptions.current, [field]: value };
};
// if the modal re-opens, we want to restart all states
useEffect(() => {
clear();
setUploadStep('import');
setSubmitting(false);
setRundown(null);
setErrors('');
}, [clear, isOpen]);
/* uploads file to backend
* - in the case of excel, we get the preview
* - in the case of project file, this is end of line
**/
const handleUpload = async () => {
let doClose = false;
if (file) {
setSubmitting(true);
setErrors('');
try {
if (isOntimeFile(file)) {
// TODO: we would also like to have preview for ontime project files
const options = ontimeFileOptions.current;
await handleOntimeFile(file, options);
await invalidateAllCaches();
doClose = true;
}
} catch (error) {
const message = maybeAxiosError(error);
setErrors(`Failed uploading file ${message}`);
} finally {
setSubmitting(false);
if (doClose) {
handleClose();
}
}
}
// when we upload project files, no extra operations are done
async function handleOntimeFile(file: File, options: Partial<ProjectFileImportOptions>) {
await uploadProjectFile(file, setProgress, options);
}
};
// before closing the modal, we clear data from mutations
const handleClose = () => {
clear();
setRundown([]);
onClose();
};
const undoReview = () => {
setUploadStep('import');
setErrors('');
};
const isImporting = uploadStep === 'import';
const isReview = uploadStep === 'review';
const isOntime = isOntimeFile(file);
const handleGoBack = isImporting ? undefined : undoReview;
const handleSubmit = handleUpload;
const disableSubmit = (isImporting && !file) || (isReview && rundown === null);
const disableGoBack = isImporting;
const submitText = isImporting ? 'Import' : 'Finish';
return (
<Modal
onClose={handleClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime-upload'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>File import</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.uploadBody}>
<>
<UploadFile />
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
</>
</ModalBody>
<ModalFooter>
<div className={style.feedbackSection}>{errors && <div className={style.error}>{errors}</div>}</div>
<div className={`${style.buttonSection} ${style.pad}`}>
<Button
onClick={handleGoBack}
isDisabled={disableGoBack || submitting}
variant='ontime-ghost-on-light'
size='sm'
>
Go Back
</Button>
<Button
onClick={handleSubmit}
isLoading={submitting}
isDisabled={disableSubmit}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
{submitText}
</Button>
</div>
</ModalFooter>
</ModalContent>
</Modal>
);
}
@@ -1,50 +0,0 @@
.uploadedItem {
margin: 0 auto;
width: 100%;
max-width: 550px;
border: 1px solid $gray-200;
padding: 0.5rem;
display: grid;
grid-template-areas:
"icon title close"
"icon info ."
"progress progress progress";
grid-template-columns: auto 1fr auto;
column-gap: 1rem;
border-radius: 3px;
.icon {
align-self: center;
grid-area: icon;
font-size: 2rem;
color: $gray-700;
}
.fileTitle {
grid-area: title;
font-size: calc(1rem - 2px);
color: $ui-black;
}
.fileInfo {
grid-area: info;
font-size: calc(1rem - 4px);
color: $gray-1100;
}
.fileProgress {
grid-area: progress;
}
.cancelUpload {
grid-area: close;
cursor: pointer;
}
&.error {
.icon {
color: $error-red;
}
}
}
@@ -1,50 +0,0 @@
import { Progress } from '@chakra-ui/react';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoDocumentTextOutline } from '@react-icons/all-files/io5/IoDocumentTextOutline';
import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline';
import { isOntimeFile } from '../../../../common/utils/uploadUtils';
import style from './UploadEntry.module.scss';
interface UploadEntryProps {
file: File | null;
errors?: string;
progress: number;
handleClear: () => void;
}
export default function UploadEntry(props: UploadEntryProps) {
const { file, errors, progress, handleClear } = props;
if (errors) {
return (
<div className={`${style.uploadedItem} ${style.error}`}>
<IoClose className={style.cancelUpload} onClick={handleClear} />
<IoWarningOutline className={style.icon} />
<span className={style.fileTitle}>{errors}</span>
<span className={style.fileInfo}>Please try again</span>
</div>
);
}
if (file) {
const fileSize = `${(file.size / 1024).toFixed(2)}kb`;
let fileType = '';
if (isOntimeFile(file)) {
fileType = 'Ontime Project File';
}
return (
<div className={style.uploadedItem}>
<IoClose className={style.cancelUpload} onClick={handleClear} />
<IoDocumentTextOutline className={style.icon} />
<span className={style.fileTitle}>{file.name}</span>
<span className={style.fileInfo}>{`${fileSize} - ${fileType}`}</span>
<Progress variant='ontime-on-light' className={style.fileProgress} value={progress} />
</div>
);
}
return null;
}
@@ -1,31 +0,0 @@
import { MutableRefObject } from 'react';
import { Switch } from '@chakra-ui/react';
import { ProjectFileImportOptions } from '../../../../common/api/ontimeApi';
import ModalSplitInput from '../../ModalSplitInput';
import style from '../UploadModal.module.scss';
interface OntimeFileOptionsProps {
optionsRef: MutableRefObject<Partial<ProjectFileImportOptions>>;
updateOptions: <T extends keyof ProjectFileImportOptions>(field: T, value: ProjectFileImportOptions[T]) => void;
}
export default function OntimeFileOptions(props: OntimeFileOptionsProps) {
const { optionsRef, updateOptions } = props;
return (
<div className={style.uploadOptions}>
<span className={style.title}>Import options</span>
<ModalSplitInput field='' title='Only import rundown' description='All other project options will be kept'>
<Switch
variant='ontime-on-light'
onChange={(e) => {
updateOptions('onlyRundown', e.target.checked);
}}
defaultChecked={Boolean(optionsRef.current.onlyRundown)}
/>
</ModalSplitInput>
</div>
);
}
@@ -1,38 +0,0 @@
@mixin row {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.25rem;
}
.stepRow {
display: flex;
gap: 2rem;
align-items: center;
margin: 0 auto;
font-size: 1rem;
}
.idle {
@include row;
color: $blue-500;
}
.inactive {
@include row;
color: $gray-700;
}
.active {
@include row;
color: $blue-700;
}
.inactiveIcon {
color: $gray-700;
}
.activeIcon {
color: $blue-500;
}
@@ -1,26 +0,0 @@
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
import { IoChevronForward } from '@react-icons/all-files/io5/IoChevronForward';
import { IoEllipseOutline } from '@react-icons/all-files/io5/IoEllipseOutline';
import type { UploadStep } from '../UploadModal';
import style from './UploadStep.module.scss';
export default function UploadStepTracker({ uploadStep }: { uploadStep: UploadStep }) {
const isImporting = uploadStep === 'import';
const isReview = uploadStep === 'review';
return (
<div className={style.stepRow}>
<div className={isImporting ? style.active : style.idle}>
<IoCheckmarkCircle />
Import
</div>
<IoChevronForward className={isReview ? style.activeIcon : style.inactiveIcon} />
<div className={isReview ? style.active : style.inactive}>
{isReview ? <IoCheckmarkCircle /> : <IoEllipseOutline />}
Review
</div>
</div>
);
}
@@ -1,21 +0,0 @@
import { create } from 'zustand';
type UploadModalContext = {
file: File | null;
setFile: (file: File | null) => void;
progress: number;
setProgress: (progress: number) => void;
clear: () => void;
};
export const useUploadModalContextStore = create<UploadModalContext>((set) => ({
file: null,
setFile: (file: File | null) => set({ file }),
progress: 0,
setProgress: (progress: number) => set({ progress }),
clear: () => set({ file: null, progress: 0 }),
}));