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';
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;
};
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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)
)
},
[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);
return;
}
}
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' });
}
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.isLoading || generate.isLoading) && (
<div className="mb-4 flex w-full justify-center">
<LoadingSpinner />
</div>
)}
{generate.isError && (
<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'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">
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<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;