import { type ProjectData } from 'ontime-types'; import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5'; import { projectLogoPath } from '../../../../common/api/constants'; import { uploadProjectLogo } from '../../../../common/api/project'; import { maybeAxiosError } from '../../../../common/api/utils'; import Button from '../../../../common/components/buttons/Button'; import Input from '../../../../common/components/input/input/Input'; import Textarea from '../../../../common/components/input/textarea/Textarea'; import useProjectData, { useUpdateProjectData } from '../../../../common/hooks-query/useProjectData'; import { preventEscape } from '../../../../common/utils/keyEvent'; import { validateLogo } from '../../../../common/utils/uploadUtils'; import { documentationUrl } from '../../../../externals'; import * as Panel from '../../panel-utils/PanelUtils'; import style from './SettingsPanel.module.scss'; type PendingLogo = { file: File; previewUrl: string; uploadedFilename?: string; }; export default function ProjectData() { const { data, status } = useProjectData(); const { updateProjectData } = useUpdateProjectData(); const [pendingLogo, setPendingLogo] = useState(null); const { handleSubmit, register, reset, formState: { isSubmitting, isValid, isDirty, errors }, setError, clearErrors, watch, control, setValue, } = useForm({ defaultValues: data, resetOptions: { keepDirtyValues: true, }, mode: 'onChange', }); const { fields, append, remove } = useFieldArray({ control, name: 'custom', }); // reset form values if data changes // Release the browser-managed preview URL when the selection changes or this form unmounts. useEffect(() => { if (data) { reset(data); } }, [data, reset]); useEffect(() => { return () => { if (pendingLogo) { URL.revokeObjectURL(pendingLogo.previewUrl); } }; }, [pendingLogo]); const handleUploadProjectLogo = (event: ChangeEvent) => { const file = event.target.files?.[0]; clearErrors('logo'); if (!file) { return; } try { validateLogo(file); setPendingLogo({ file, previewUrl: URL.createObjectURL(file), }); } catch (error) { const message = maybeAxiosError(error); setError('logo', { message }); } finally { // Allow selecting the same local file again after an upload attempt. event.target.value = ''; } }; const { ref, ...projectLogoRest } = register('logo'); const uploadInputRef = useRef(null); const handleClickUpload = () => { uploadInputRef.current?.click(); }; const handleDeleteLogo = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setPendingLogo(null); setValue('logo', null, { shouldDirty: true, }); }; const handleAddCustom = () => { append({ title: '', value: '', url: '' }); }; const uploadPendingLogo = async () => { if (!pendingLogo) { return null; } if (pendingLogo.uploadedFilename) { return pendingLogo.uploadedFilename; } const response = await uploadProjectLogo(pendingLogo.file); const uploadedFilename = response.data.logoFilename; setPendingLogo((currentPendingLogo) => currentPendingLogo?.file === pendingLogo.file ? { ...currentPendingLogo, uploadedFilename } : currentPendingLogo, ); return uploadedFilename; }; const onSubmit = async (formData: ProjectData) => { try { clearErrors(); const logo = (await uploadPendingLogo()) ?? formData.logo; await updateProjectData({ ...formData, logo }); setPendingLogo(null); } catch (error) { const message = maybeAxiosError(error); setError('root', { message }); } }; // reset data to the initial state const onReset = () => { setPendingLogo(null); reset(data); }; const isLoading = status === 'pending'; const logo = watch('logo'); return ( preventEscape(event, onReset)}> Project data