diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx index 9ac80b02b7..fd78b382af 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx @@ -1,6 +1,7 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' +import { Flex, Skeleton } from '@chakra-ui/react' -import { FormAuthType, FormLogoState } from '~shared/types' +import { FormAuthType, FormLogoState, FormStartPage } from '~shared/types' import { useEnv } from '~features/env/queries' import { FormBannerLogo } from '~features/public-form/components/FormStartPage/FormBannerLogo' @@ -9,34 +10,87 @@ import { useFormBannerLogo } from '~features/public-form/components/FormStartPag import { useFormHeader } from '~features/public-form/components/FormStartPage/useFormHeader' import { useCreateTabForm } from '../useCreateTabForm' -import { startPageDataSelector, useDesignStore } from '../useDesignStore' +import { + customLogoMetaSelector, + startPageDataSelector, + useDesignStore, +} from '../useDesignStore' export const StartPageView = () => { const { data: form } = useCreateTabForm() - const startPageFromStore = useDesignStore(startPageDataSelector) + const { startPageData, customLogoMeta } = useDesignStore((state) => ({ + startPageData: startPageDataSelector(state), + customLogoMeta: customLogoMetaSelector(state), + })) const { data: { logoBucketUrl } = {} } = useEnv( form?.startPage.logo.state === FormLogoState.Custom, ) + const [customLogoPending, setCustomLogoPending] = useState(false) + + // Transform the FormStartPageInput into a FormStartPage + const startPageFromStore: FormStartPage | null = useMemo(() => { + if (!startPageData) return null + const { logo, estTimeTaken, ...rest } = startPageData + const estTimeTakenTransformed = + estTimeTaken === '' ? undefined : estTimeTaken + if (logo.state !== FormLogoState.Custom) { + setCustomLogoPending(false) + return { + logo: { state: logo.state }, + estTimeTaken: estTimeTakenTransformed, + ...rest, + } + } + setCustomLogoPending(!startPageData?.attachment.srcUrl) + return { + logo: { + state: FormLogoState.Custom, + // Placeholder values + fileId: customLogoMeta?.fileId ?? '', + fileName: customLogoMeta?.fileName ?? '', + fileSizeInBytes: customLogoMeta?.fileSizeInBytes ?? 0, + }, + estTimeTaken: estTimeTakenTransformed, + ...rest, + } + }, [startPageData, customLogoMeta]) + // When drawer is opened, store is populated. We always want the drawer settings // to be previewed, so when the store is populated, prioritize that setting. - const startPage = useMemo( - () => (startPageFromStore ? startPageFromStore : form?.startPage), - [startPageFromStore, form?.startPage], - ) + const startPage = useMemo(() => { + if (startPageFromStore) return startPageFromStore + setCustomLogoPending(false) + return form?.startPage + }, [form?.startPage, startPageFromStore]) // Color theme options and other design stuff, identical to public form const { titleColor, titleBg, estTimeString } = useFormHeader(startPage) - const formBannerLogoProps = useFormBannerLogo({ - logoBucketUrl, // This will be conditional once the logo field is added. + const { hasLogo, logoImgSrc, logoImgAlt } = useFormBannerLogo({ + logoBucketUrl, logo: startPage?.logo, agency: form?.admin.agency, }) return ( <> - + {customLogoPending ? ( + // Show skeleton if user has chosen custom logo but not yet uploaded + + + + ) : ( + + )} { + const toast = useToast({ status: 'danger' }) const { data: form } = useCreateTabForm() + const { formId } = useParams() + if (!formId) throw new Error('No formId provided') - const isMobile = useIsMobile() const { startPageMutation } = useMutateFormPage() + const { data: { logoBucketUrl } = {} } = useEnv() + const { handleClose } = useCreatePageSidebar() - const closeBuilderDrawer = useBuilderAndDesignStore(setToInactiveSelector) - const { setDesignState, resetDesignState } = useDesignStore((state) => ({ - setDesignState: setStartPageDataSelector(state), - resetDesignState: resetStartPageDataSelector(state), - })) + const [existingCustomLogoFetched, setExistingCustomLogoFetched] = + useState(form?.startPage.logo.state !== FormLogoState.Custom) - // Load the start page into the store when use opens the drawer - useEffect(() => { - if (form) setDesignState(form.startPage) - return () => resetDesignState() - }, [form, setDesignState, resetDesignState]) + const isLoading = useMemo( + () => startPageMutation.isLoading || !existingCustomLogoFetched, + [startPageMutation.isLoading, existingCustomLogoFetched], + ) + + const { + startPageData, + customLogoMeta, + setStartPageData, + setAttachment, + setCustomLogoMeta, + resetDesignStore, + } = useDesignStore((state) => ({ + startPageData: startPageDataSelector(state), + customLogoMeta: customLogoMetaSelector(state), + setStartPageData: setStartPageDataSelector(state), + setAttachment: setAttachmentSelector(state), + setCustomLogoMeta: setCustomLogoMetaSelector(state), + resetDesignStore: resetDesignStoreSelector(state), + })) const { register, - formState: { errors }, + formState: { errors, isDirty }, control, handleSubmit, - } = useForm({ + resetField, + clearErrors, + setError, + } = useForm({ mode: 'onBlur', - defaultValues: form?.startPage, + defaultValues: { ...form?.startPage, attachment: {} }, }) - // Save design functions - const handleDesignChanges = useCallback( - (startPageInputs) => { - setDesignState({ ...(startPageInputs as FormStartPage) }) + // On mount, fetch custom logo file to display as part of attachment field. + const setAttachmentOnMount = useCallback( + async (logo: CustomFormLogo) => { + const srcUrl = `${logoBucketUrl}/${logo.fileId}` + const customLogoBlob = await fetch(srcUrl).then((res) => res.blob()) + setAttachment({ + file: new File([customLogoBlob], logo.fileName), + srcUrl, + }) + setExistingCustomLogoFetched(true) }, - [setDesignState], + [logoBucketUrl, setAttachment], + ) + + // Load existing start page and custom logo into form when user opens drawer + useEffect(() => { + setStartPageData({ + ...form?.startPage, + attachment: {}, + } as FormStartPageInput) + if (form?.startPage.logo.state === FormLogoState.Custom) { + setAttachmentOnMount(form?.startPage.logo) + setCustomLogoMeta(form?.startPage.logo) + } + return () => resetDesignStore() + }, []) + + useEffect( + () => + resetField('attachment', { + defaultValue: startPageData?.attachment, + }), + [existingCustomLogoFetched], ) const watchedInputs = useWatch({ control: control, - }) as UnpackNestedValue + }) as UnpackNestedValue const clonedWatchedInputs = useMemo( () => cloneDeep(watchedInputs), [watchedInputs], ) - useDebounce(() => handleDesignChanges(clonedWatchedInputs), 300, [ - Object.values(clonedWatchedInputs), + useDebounce(() => setStartPageData(clonedWatchedInputs), 300, [ + clonedWatchedInputs, ]) - const handleUpdateDesign = handleSubmit((startPage) => - startPageMutation.mutate(startPage), + // Save design handlers + const uploadLogoMutation = useMutation((image: File) => + uploadLogo({ formId, image }), + ) + + const handleUploadLogo = useCallback( + (attachment: UploadedImage): Promise | CustomLogoMeta => { + if (!attachment.file || !attachment.srcUrl) + throw new Error('Design pre-submit validation failed') + if (!attachment.srcUrl.startsWith('blob:')) { + // Logo was not changed + if (!customLogoMeta) + throw new Error('Design: customLogoMeta is undefined') + return customLogoMeta + } + return uploadLogoMutation + .mutateAsync(attachment.file) + .then((uploadedFileData) => { + return { + fileName: uploadedFileData.name, + fileId: uploadedFileData.fileId, + fileSizeInBytes: uploadedFileData.size, + } + }) + }, + [uploadLogoMutation, customLogoMeta], ) - if (!form) return null + const handleUpdateDesign = handleSubmit( + async (startPageData: FormStartPageInput) => { + const { logo, attachment, estTimeTaken, ...rest } = startPageData + const estTimeTakenTransformed = + estTimeTaken === '' ? undefined : estTimeTaken + if (logo.state !== FormLogoState.Custom) + startPageMutation.mutate( + { + logo: { state: logo.state }, + estTimeTaken: estTimeTakenTransformed, + ...rest, + }, + { onSuccess: handleClose }, + ) + else { + const customLogoMeta = await handleUploadLogo(attachment) + startPageMutation.mutate( + { + logo: { state: FormLogoState.Custom, ...customLogoMeta }, + estTimeTaken: estTimeTakenTransformed, + ...rest, + }, + { onSuccess: handleClose }, + ) + } + }, + ) + + const handleClick = useCallback(async () => { + handleUpdateDesign().catch((error) => { + toast({ description: error.message }) + }) + }, [handleUpdateDesign, toast]) + + if (!startPageData) return null return ( @@ -108,16 +226,110 @@ export const DesignDrawer = (): JSX.Element | null => { + + Logo + + + Default + + + None + + + Upload custom logo (jpg, png, or gif) + + + + + + + + + Theme colour + + + {Object.values(FormColorTheme).map((color) => ( + + ))} + + + {errors.colorTheme?.message} + + + Time taken to complete form (minutes) { {errors.estTimeTaken?.message} - + Instructions for your form