From 256ef5792c7d6183a6aee4a9a439b2a64930579a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jul 2023 15:02:29 +1000 Subject: [PATCH] Render basic field stack, and extract field data from API --- src/frontend/src/components/forms/ApiForm.tsx | 148 +++++++++++++++--- src/frontend/src/pages/Index/Home.tsx | 5 +- 2 files changed, 126 insertions(+), 27 deletions(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index c04b49d48c..79779c6cda 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -1,7 +1,8 @@ import { t } from '@lingui/macro'; -import { Divider, Modal } from '@mantine/core'; +import { Alert, Divider, Modal } from '@mantine/core'; import { Button, Center, Group, Loader, Stack, Text } from '@mantine/core'; import { useForm } from '@mantine/form'; +import { IconAlertCircle } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { AxiosResponse } from 'axios'; import { useEffect } from 'react'; @@ -15,12 +16,13 @@ import { api } from '../../App'; * - All other attributes are optional, and may be provided by the API * - However, they can be overridden by the user */ -export type ApiFormField = { +export type ApiFormFieldType = { name: string; label?: string; value?: any; type?: string; required?: boolean; + hidden?: boolean; placeholder?: string; help_text?: string; icon?: string; @@ -28,13 +30,21 @@ export type ApiFormField = { }; /** - * Extract a list of field definitions from an API response. - * @param response : The API response to extract the field definitions from. - * @returns A list of field definitions. + * Render an individual form field */ -function extractFieldDefinitions(response: AxiosResponse): ApiFormField[] { - // TODO: []; - return []; +function ApiFormField({ + field, + definitions +}: { + field: ApiFormFieldType; + definitions: ApiFormFieldType[]; +}) { + useEffect(() => { + console.log('field:', field); + console.log('definitions:', definitions); + }, []); + + return {field.name}; } /** @@ -58,15 +68,17 @@ export function ApiForm({ onFormSuccess, onFormError, cancelText = t`Cancel`, - submitText = t`Submit` + submitText = t`Submit`, + method = 'PUT' }: { name: string; url: string; pk?: number; title: string; - fields: ApiFormField[]; + fields: ApiFormFieldType[]; cancelText?: string; submitText?: string; + method?: string; opened: boolean; onClose?: () => void; onFormSuccess?: () => void; @@ -76,35 +88,96 @@ export function ApiForm({ const form = useForm({}); // Form field definitions (via API) - const [fieldDefinitions, setFieldDefinitions] = useState([]); + const [fieldDefinitions, setFieldDefinitions] = useState( + [] + ); + + // Error observed during form construction + const [error, setError] = useState(''); const definitionQuery = useQuery({ enabled: opened && !!url, queryKey: ['form-definition', name, url, pk, fields], queryFn: async () => { - let _url = url; - - if (pk && pk > 0) { - _url += `${pk}/`; - } - console.log('Fetching form definition from API:', _url); - + // Clear form construction error field + setError(''); return api - .options(_url) + .options(getUrl()) .then((response) => { - console.log('response:', response); setFieldDefinitions(extractFieldDefinitions(response)); return response; }) .catch((error) => { - console.error('error:', error); + console.error('Error fetching field definitions:', error); + setError(error.message); setFieldDefinitions([]); }); } }); + // State variable to determine if the form can render the data + const [canRender, setCanRender] = useState(false); + + // Update the canRender state variable on status change + useEffect(() => { + setCanRender( + !definitionQuery.isFetching && definitionQuery.isSuccess && !error + ); + }, [definitionQuery, error]); + + // Construct a fully-qualified URL based on the provided details + function getUrl(): string { + let u = url; + + if (pk && pk > 0) { + u += `${pk}/`; + } + + return u; + } + + /** + * Extract a list of field definitions from an API response. + * @param response : The API response to extract the field definitions from. + * @returns A list of field definitions. + */ + function extractFieldDefinitions( + response: AxiosResponse + ): ApiFormFieldType[] { + let actions = response.data?.actions[method.toUpperCase()] || []; + + if (actions.length == 0) { + setError(`Permission denied for ${method} at ${url}`); + return []; + } + + let definitions: ApiFormFieldType[] = []; + + for (const fieldName in actions) { + const field = actions[fieldName]; + definitions.push({ + name: fieldName, + label: field.label, + help_text: field.help_text, + value: field.value, + type: field.type, + required: field.required, + placeholder: field.placeholder, + icon: field.icon + }); + } + + return definitions; + } + return ( - + {definitionQuery.isFetching && ( @@ -112,13 +185,38 @@ export function ApiForm({ )} - {!definitionQuery.isFetching && Form definition fetched!} + {error && ( + } + > + {error} + + )} + {canRender && ( + + {fields.map((field) => ( + + ))} + + )} - - diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index 00e32b5ea4..6ba15cc198 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -3,14 +3,14 @@ import { Group } from '@mantine/core'; import { Button } from '@mantine/core'; import { useState } from 'react'; -import { ApiForm, ApiFormField } from '../../components/forms/ApiForm'; +import { ApiForm, ApiFormFieldType } from '../../components/forms/ApiForm'; import { PlaceholderPill } from '../../components/items/Placeholder'; import { StylishText } from '../../components/items/StylishText'; export default function Home() { const [formOpened, setFormOpened] = useState(false); - const fields: ApiFormField[] = [ + const fields: ApiFormFieldType[] = [ { name: 'name' }, @@ -31,6 +31,7 @@ export default function Home() { name="part-edit" url="/part/1/" fields={fields} + method="PUT" title="Edit Part" opened={formOpened} onClose={() => setFormOpened(false)}