diff --git a/apps/client/src/features/modals/upload-modal/UploadFile.tsx b/apps/client/src/features/modals/upload-modal/UploadFile.tsx index 958aba9ed..b6a3f418a 100644 --- a/apps/client/src/features/modals/upload-modal/UploadFile.tsx +++ b/apps/client/src/features/modals/upload-modal/UploadFile.tsx @@ -17,18 +17,25 @@ export default function UploadFile() { const clearFile = () => { setFile(null); + setErrors(''); }; const handleFile = (event: ChangeEvent) => { - const fileSelected = event?.target?.files?.[0]; - if (!fileSelected) return; + setErrors(''); - const validate = validateFile(fileSelected); - setErrors(validate.errors?.[0]); + const selectedFile = event?.target?.files?.[0]; + if (!selectedFile) { + setFile(null); + return; + } - if (validate.isValid) { - setFile(fileSelected); - } else { + try { + validateFile(selectedFile); + setFile(selectedFile); + } catch (error) { + if (error instanceof Error) { + setErrors(error.message); + } setFile(null); } }; @@ -47,9 +54,11 @@ export default function UploadFile() { accept='.json, .xlsx' data-testid='file-input' /> -
- Click to upload Ontime project or xlsx file -
+ {!file && ( +
+ Click to upload Ontime project or xlsx file +
+ )} {(file || errors) && ( )} diff --git a/apps/client/src/features/modals/upload-modal/UploadModal.tsx b/apps/client/src/features/modals/upload-modal/UploadModal.tsx index a462043ad..119dda048 100644 --- a/apps/client/src/features/modals/upload-modal/UploadModal.tsx +++ b/apps/client/src/features/modals/upload-modal/UploadModal.tsx @@ -10,14 +10,18 @@ import { ModalOverlay, } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; -import { OntimeRundown } from 'ontime-types'; +import { OntimeRundown, ProjectData, UserFields } from 'ontime-types'; +import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils'; -import { RUNDOWN_TABLE } from '../../../common/api/apiConstants'; -import { maybeAxiosError } from '../../../common/api/apiUtils'; -import { postPreviewExcel, uploadData } from '../../../common/api/ontimeApi'; +import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils'; +import { + patchData, + postPreviewExcel, + ProjectFileImportOptions, + uploadProjectFile, +} from '../../../common/api/ontimeApi'; import { projectDataPlaceholder } from '../../../common/models/ProjectData'; import { userFieldsPlaceholder } from '../../../common/models/UserFields'; -import { cx } from '../../../common/utils/styleUtils'; import PreviewExcel from './preview/PreviewExcel'; import ExcelFileOptions from './upload-options/ExcelFileOptions'; @@ -25,20 +29,12 @@ import OntimeFileOptions from './upload-options/OntimeFileOptions'; import UploadStepTracker from './upload-step/UploadStep'; import UploadFile from './UploadFile'; import { useUploadModalContextStore } from './uploadModalContext'; -import { defaultExcelImportMap, ExcelImportMapKeys, isExcelFile, isOntimeFile } from './uploadUtils'; +import { isExcelFile, isOntimeFile } from './uploadUtils'; import style from './UploadModal.module.scss'; export type UploadStep = 'upload' | 'review'; -export interface OntimeInputOptions { - onlyImportRundown?: boolean; -} - -export type ExcelInputOptions = { - [K in ExcelImportMapKeys]: string; -}; - interface UploadModalProps { onClose: () => void; isOpen: boolean; @@ -51,67 +47,76 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) { const [uploadStep, setUploadStep] = useState('upload'); const [submitting, setSubmitting] = useState(false); - const [rundown, setRundown] = useState([]); - const [userFields, setUserFields] = useState(userFieldsPlaceholder); - const [project, setProject] = useState(projectDataPlaceholder); + const [rundown, setRundown] = useState(null); + const [userFields, setUserFields] = useState(null); + const [project, setProject] = useState(null); const [errors, setErrors] = useState(''); - const ontimeFileOptions = useRef>({}); - const excelFileOptions = useRef>(defaultExcelImportMap); + const ontimeFileOptions = useRef>({}); + const excelFileOptions = useRef(defaultExcelImportMap); + /* if the modal re-opens, we want to restart all states */ useEffect(() => { clear(); setUploadStep('upload'); setSubmitting(false); - setRundown([]); - setUserFields(userFieldsPlaceholder); - setProject(projectDataPlaceholder); + setRundown(null); + setUserFields(null); + setProject(null); setErrors(''); }, [clear, isOpen]); - const handleParse = async () => { + /* 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)) { - await handleOntimeFile(file); - await queryClient.invalidateQueries(RUNDOWN_TABLE); + // TODO: we would also like to have preview for ontime project files + const options = ontimeFileOptions.current; + await handleOntimeFile(file, options); + doClose = true; } else if (isExcelFile(file)) { - await handleExcelFile(file); - } - } catch (error) { - setErrors(`Failed uploading file: ${error}`); - } finally { - setSubmitting(false); - } - } - - async function handleExcelFile(file: File) { - const options = excelFileOptions.current; - // TODO: option type should be central, to also be used by backend - try { - const response = await postPreviewExcel(file, setProgress, options); - if (response.status === 200) { - setRundown(response.data.rundown); - setUserFields(response.data.userFields); - setProject(response.data.project); - setUploadStep('review'); + const options = excelFileOptions.current; + await handleExcelFile(file, options); + await invalidateAllCaches(); } } catch (error) { const message = maybeAxiosError(error); - setErrors(`Error importing excel ${message}`); + setErrors(`Failed uploading file ${message}`); + } finally { + setSubmitting(false); + if (doClose) { + handleClose(); + } } } - async function handleOntimeFile(file: File) { - const options = { - onlyRundown: Boolean(ontimeFileOptions.current.onlyImportRundown), - }; - await uploadData(file, setProgress, options); + // when we upload excel, we populate state with preview data + async function handleExcelFile(file: File, options: ExcelImportMap) { + const response = await postPreviewExcel(file, setProgress, options); + if (response.status === 200) { + setRundown(response.data.rundown); + setUserFields(response.data.userFields); + setProject(response.data.project); + // in excel imports we have an extra review step + setUploadStep('review'); + } + } + + // when we upload project files, no extra operations are done + async function handleOntimeFile(file: File, options: Partial) { + await uploadProjectFile(file, setProgress, options); } }; + // before closing the modal, we clear data from mutations const handleClose = () => { clear(); setRundown([]); @@ -121,36 +126,42 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) { }; const handleFinalise = async () => { - if (file) { + // this step is currently only used for excel files, after preview + if (isExcel && rundown && userFields && project) { + let doClose = false; setSubmitting(true); try { - const options = { - //onlyRundown: overrideOptionRef.current?.checked || false, - }; - await uploadData(file, setProgress, options); - handleClose(); + await patchData({ rundown, userFields, project }); + await queryClient.invalidateQueries(['rundown', 'userFields', 'project']); + doClose = true; } catch (error) { - console.error(error); + const message = maybeAxiosError(error); + setErrors(`Failed applying changes ${message}`); } finally { - await queryClient.invalidateQueries(RUNDOWN_TABLE); setSubmitting(false); + if (doClose) { + handleClose(); + } } } }; + const undoReview = () => { + setUploadStep('upload'); + setErrors(''); + }; + const isUpload = uploadStep === 'upload'; + const isReview = uploadStep === 'review'; const isExcel = isExcelFile(file); const isOntime = isOntimeFile(file); - const handleGoBack = isUpload ? undefined : () => setUploadStep('upload'); - const handleSubmit = isUpload ? handleParse : handleFinalise; - const disableSubmit = isUpload && !file; + const handleGoBack = isUpload ? undefined : undoReview; + const handleSubmit = isUpload ? handleUpload : handleFinalise; + const disableSubmit = (isUpload && !file) || (isReview && rundown === null); const disableGoBack = isUpload; const submitText = isUpload ? 'Upload' : 'Finish'; - const modalClasses = cx([style.modalWidthOverride, isExcel ? style.doExtend : null]); - - console.log('debug', isExcel, modalClasses); return ( - {errors &&
{errors}
} {isOntime && } {isExcel && } ) : ( - + )} - - - + +
{errors &&
{errors}
}
+ +
+ + +
diff --git a/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx b/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx index 21e37c35c..f78ea2374 100644 --- a/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx +++ b/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx @@ -1,35 +1,32 @@ import { MutableRefObject } from 'react'; import { Switch } from '@chakra-ui/react'; +import { ProjectFileImportOptions } from '../../../../common/api/ontimeApi'; import ModalSplitInput from '../../ModalSplitInput'; -import { OntimeInputOptions } from '../UploadModal'; import style from '../UploadModal.module.scss'; interface OntimeFileOptionsProps { - optionsRef: MutableRefObject; + optionsRef: MutableRefObject>; } export default function OntimeFileOptions(props: OntimeFileOptionsProps) { const { optionsRef } = props; - const updateRef = (field: T, value: OntimeInputOptions[T]) => { + const updateRef = (field: T, value: ProjectFileImportOptions[T]) => { optionsRef.current = { ...optionsRef.current, [field]: value }; }; return (
Import options - + { - updateRef('onlyImportRundown', e.target.checked); + updateRef('onlyRundown', e.target.checked); }} + defaultChecked={Boolean(optionsRef.current.onlyRundown)} />
diff --git a/apps/client/src/features/modals/upload-modal/uploadUtils.ts b/apps/client/src/features/modals/upload-modal/uploadUtils.ts index 0798f97fe..16fc8b0eb 100644 --- a/apps/client/src/features/modals/upload-modal/uploadUtils.ts +++ b/apps/client/src/features/modals/upload-modal/uploadUtils.ts @@ -1,33 +1,22 @@ -type ValidationStatus = { - errors: string[]; - isValid: boolean; -}; - -export function validateFile(file: File): ValidationStatus { - const status: ValidationStatus = { errors: [], isValid: true }; +export function validateFile(file: File) { if (!file) { - status.errors.push('No file to upload'); - status.isValid = false; + throw new Error('No file to upload'); } // Limit file size of a project file to around 1MB if (file.name.endsWith('.json') && file.size > 1_000_000) { - status.errors.push('File size limit (1MB) exceeded'); - status.isValid = false; + 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) { - status.errors.push('File size limit (10MB) exceeded'); - status.isValid = false; + throw new Error('File size limit (10MB) exceeded'); } // Check file extension if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) { - status.errors.push('Unhandled file type'); - status.isValid = false; + throw new Error('Unhandled file type'); } - return status; } export function isExcelFile(file: File | null) { diff --git a/apps/server/src/classes/data-provider/DataProvider.ts b/apps/server/src/classes/data-provider/DataProvider.ts index f155ba374..ea5285b9b 100644 --- a/apps/server/src/classes/data-provider/DataProvider.ts +++ b/apps/server/src/classes/data-provider/DataProvider.ts @@ -2,7 +2,16 @@ * Class Event Provider is a mediator for handling the local db * and adds logic specific to ontime data */ -import { ProjectData, OntimeRundown, ViewSettings } from 'ontime-types'; +import { + ProjectData, + OntimeRundown, + ViewSettings, + DatabaseModel, + OSCSettings, + UserFields, + Alias, + Settings, +} from 'ontime-types'; import { data, db } from '../../modules/loadDb.js'; import { safeMerge } from './DataProvider.utils.js'; @@ -45,7 +54,7 @@ export class DataProvider { return data.settings; } - static async setSettings(newData) { + static async setSettings(newData: Settings) { data.settings = { ...newData }; await this.persist(); } @@ -58,7 +67,7 @@ export class DataProvider { return data.aliases; } - static async setAliases(newData) { + static async setAliases(newData: Alias[]) { data.aliases = newData; await this.persist(); } @@ -76,12 +85,12 @@ export class DataProvider { await this.persist(); } - static async setUserFields(newData) { + static async setUserFields(newData: UserFields) { data.userFields = { ...newData }; await this.persist(); } - static async setOsc(newData) { + static async setOsc(newData: OSCSettings) { data.osc = { ...newData }; await this.persist(); } @@ -95,7 +104,7 @@ export class DataProvider { await db.write(); } - static async mergeIntoData(newData) { + static async mergeIntoData(newData: Partial) { const mergedData = safeMerge(data, newData); data.project = mergedData.project; data.settings = mergedData.settings; diff --git a/apps/server/src/classes/data-provider/DataProvider.utils.ts b/apps/server/src/classes/data-provider/DataProvider.utils.ts index f2ea10d68..eed142912 100644 --- a/apps/server/src/classes/data-provider/DataProvider.utils.ts +++ b/apps/server/src/classes/data-provider/DataProvider.utils.ts @@ -1,10 +1,12 @@ +import { DatabaseModel } from 'ontime-types'; + /** * Merges two data objects * @param {object} existing * @param {object} newData */ -export function safeMerge(existing, newData) { - const { rundown, project, settings, viewSettings, osc, http, aliases, userFields } = newData || {}; +export function safeMerge(existing: DatabaseModel, newData: Partial) { + const { rundown, project, settings, viewSettings, osc, aliases, userFields } = newData || {}; return { ...existing, rundown: rundown ?? existing.rundown, @@ -32,6 +34,5 @@ export function safeMerge(existing, newData) { : {}), }, }, - http: { ...existing.http, ...http }, }; } diff --git a/apps/server/src/classes/data-provider/__test__/DataProvider.test.ts b/apps/server/src/classes/data-provider/__test__/DataProvider.test.ts index 6a6f85a77..e17b019da 100644 --- a/apps/server/src/classes/data-provider/__test__/DataProvider.test.ts +++ b/apps/server/src/classes/data-provider/__test__/DataProvider.test.ts @@ -42,11 +42,6 @@ describe('safeMerge', () => { onFinish: [], }, }, - http: { - enabled: true, - user: null, - pwd: null, - }, }; it('returns existing data if new data is not provided', () => { @@ -188,19 +183,6 @@ describe('safeMerge', () => { onFinish: [], }, }, - http: { - user: null, - pwd: null, - messages: { - onLoad: [], - onStart: [], - onUpdate: [], - onPause: [], - onStop: [], - onFinish: [], - }, - enabled: true, - }, }; const newData = { diff --git a/apps/server/src/modules/loadDb.ts b/apps/server/src/modules/loadDb.ts index 9de1cccd5..4b831f131 100644 --- a/apps/server/src/modules/loadDb.ts +++ b/apps/server/src/modules/loadDb.ts @@ -42,7 +42,7 @@ const parseDb = async (fileToRead, adapterToUse) => { adapterToUse.data = dbModel; } - return parseJson(adapterToUse.data, true); + return parseJson(adapterToUse.data); }; /** diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index 9c33e9168..8da7f6642 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -287,13 +287,13 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?: updateTimer(); } - if (options.external) { - // advice socket subscribers of change - sendRefetch(); - } - if (options.reset) { // force rundown to be recalculated forceReset(); } + + if (options.external) { + // advice socket subscribers of change + sendRefetch(); + } }