Skip to content
Snippets Groups Projects
TemplateForm.tsx 8.1 KiB
Newer Older
import { type CutterField, type Template as TemplateDto } from 'lib/client/index';
import {
    type FC,
    type FormEventHandler,
    useCallback,
    useLayoutEffect,
    useRef,
    useState,
} from 'react';
import { useAuth } from 'react-oidc-context';
import { useProjectApi } from 'lib/useApi';
import { useMutation, useQuery } from '@tanstack/react-query';
import { type AxiosRequestConfig } from 'axios';
import Form from 'components/template/Form';
import Button from 'components/Button';
import LoadingSpinner from 'components/LoadingSpinner';
Christophe's avatar
Christophe committed
import TemplateGenerationError from 'components/TemplateGenerationError';

const hasDefaultValue = (field: CutterField) => {
    // TODO: type assertion because of api spec/generator issue
    const defaultValue = field.default as string;

    return defaultValue.length > 0;
};

type TemplateFormProps = {
    template: TemplateDto;
};
const TemplateForm: FC<TemplateFormProps> = ({ template }) => {
    const auth = useAuth();
    const api = useProjectApi();

    const fields = useQuery(['fields', template.id], () => api.fetchFields(template.id));

    const generate = useMutation(
        ['generate', template.id],
        (data: Record<string, string>) =>
            api.generateProject(template.id, data, {
                responseType: 'blob',
            } as AxiosRequestConfig),
        {
            onSuccess: (data) => {
                const link = document.createElement('a');
                const blob = new Blob([data.data], { type: 'application/zip' });
                link.href = URL.createObjectURL(blob);
                link.download =
                    data.headers['content-disposition']?.replace(/attachment;\s*filename=/, '') ??
                    'cookiecutter.zip';
                link.click();
            },
        }
    );

    const formSubmitButton = useRef<HTMLButtonElement>(null);
    const missingFieldsModal = useRef<HTMLDialogElement>(null);

    const [overrideMissingFieldsWarning, setOverrideMissingFieldsWarning] = useState(false);
    const [emptyFields, setEmptyFields] = useState<string[]>([]);

    const findMissingFields = useCallback(
        (form: FormData) => {
            if (!fields.isSuccess) {
                throw new Error("Can't validate missing fields without fields");
            }

            // all fields that expect a value without default
            const keysToCheck = fields.data.data
                .filter((f) => f.type !== 'checkbox' && !hasDefaultValue(f))
                .map((f) => f.name);
            // all fields that are empty
            return Array.from(form.entries())
                .filter(
                    ([key, value]) =>
                        keysToCheck.includes(key) &&
                        (typeof value !== 'string' || value.length === 0)
                )
                .map(([key]) => key);
        },
        [fields.isSuccess, fields.data]
    );

    const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
        e.preventDefault();
        if (!fields.isSuccess || auth.user?.access_token === undefined) {
            return;
        }

        const form = new FormData(e.currentTarget);

        if (!overrideMissingFieldsWarning) {
            const _emptyFields = findMissingFields(form);
            if (_emptyFields.length !== 0) {
                setEmptyFields(_emptyFields);
                missingFieldsModal.current?.showModal();
                setOverrideMissingFieldsWarning(true);
                console.warn('Missing fields:', _emptyFields);
        const entries = Array.from(form.entries()).filter(
            ([, value]) => typeof value === 'string' && value.length > 0
        );
        // TODO: get rid of type assertion, asserting because we have no files
        const json = Object.fromEntries(entries) as Record<string, string>;
        generate.mutate(json);
    };

    useLayoutEffect(() => {
        if (generate.isError && generate.error) {
            document.getElementById('something-went-wrong')?.scrollIntoView({ behavior: 'smooth' });
        }
    }, [generate.error, generate.isError]);

    return (
        <div>
            <form onSubmit={handleSubmit} className="mb-0">
                <p>
                    Filling this web-form will generate a .zip file with the folders generated by
                    the cookiecutter.
                </p>
                <div>
                    {fields.isSuccess && (
                        <>
                            <Form fields={fields.data.data} flaggedFields={emptyFields} />
                            <div className="flex justify-center pt-2">
                                {auth.isAuthenticated && (
                                    <Button
                                        type="submit"
                                        ref={formSubmitButton}
                                        disabled={generate.isLoading}
                                        variant={generate.isError ? 'warning' : undefined}
                                    >
                                        Generate
                                    </Button>
                                )}
                                {!auth.isAuthenticated && (
                                    <Button
                                        type="button"
                                        onClick={() => auth.signinRedirect()}
                                        variant="secondary"
                                    >
                                        Login
                                    </Button>
                                )}
                            </div>
                        </>
                    )}
                </div>
                {fields.isError && (
Christophe's avatar
Christophe committed
                    <TemplateGenerationError error={fields.error}>
                        <p>Failed to load template fields:</p>
Christophe's avatar
Christophe committed
                    </TemplateGenerationError>
                {(fields.isLoading || generate.isLoading) && (
                    <div className="mb-4 flex w-full justify-center">
                        <LoadingSpinner />
                    </div>
                )}
                {generate.isError && (
Christophe's avatar
Christophe committed
                    <TemplateGenerationError
                        error={generate.error}
                        template={template}
                        className="mt-2"
                    >
                        <p id="something-went-wrong">Failed to generate the project:</p>
                    </TemplateGenerationError>
                )}
            </form>
            <dialog id="missing-fields" ref={missingFieldsModal} className="modal p-3">
                <p>
                    It looks like you haven&apos;t filled out all the fields. Are you sure you want
                    to submit the form?{' '}
                </p>
                <p>Missing fields:</p>
                <ul>
                    {emptyFields.map((f) => (
                        <li key={f} style={{ marginBlock: '.1rem', listStyle: 'disc inside' }}>
                            {f}
                        </li>
                    ))}
                </ul>
                <div className="flex-gap flex justify-end">
                    <Button
                        variant="secondary"
                        onClick={() => {
                            missingFieldsModal.current?.close();
                            setOverrideMissingFieldsWarning(false);
                        }}
                    >
                        Cancel
                    </Button>
                    <Button
                        variant="warning"
                        onClick={() => {
                            missingFieldsModal.current?.close();
                            // HACK: manually retrigger submission here, find a more proper way?
                            formSubmitButton.current?.click();
                            setOverrideMissingFieldsWarning(false);
                        }}
                    >
                        Submit
                    </Button>
                </div>
            </dialog>
        </div>
    );
};

export default TemplateForm;