diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 81c8004ea0..4fc6e8b5f6 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -32,6 +32,7 @@ export type ApiFormFieldType = { name: string; label?: string; value?: any; + default?: any; icon?: ReactNode; fieldType?: string; api_url?: string; @@ -249,8 +250,7 @@ function ApiFormField({ } /** - * An ApiForm component is a modal form which is rendered dynamically, - * based on an API endpoint. + * Properties for the ApiForm component * @param url : The API endpoint to fetch the form from. * @param fields : The fields to render in the form. * @param opened : Whether the form is opened or not. @@ -258,21 +258,7 @@ function ApiFormField({ * @param onFormSuccess : A callback function to call when the form is submitted successfully. * @param onFormError : A callback function to call when the form is submitted with errors. */ -export function ApiForm({ - name, - url, - pk, - title, - fields, - opened, - onClose, - onFormSuccess, - onFormError, - cancelText = t`Cancel`, - submitText = t`Submit`, - method = 'PUT', - fetchInitialData = false -}: { +export interface ApiFormProps { name: string; url: string; pk?: number; @@ -280,13 +266,22 @@ export function ApiForm({ fields: ApiFormFieldType[]; cancelText?: string; submitText?: string; + submitColor?: string; + cancelColor?: string; fetchInitialData?: boolean; method?: string; opened: boolean; onClose?: () => void; onFormSuccess?: () => void; onFormError?: () => void; -}) { +} + +/** + * An ApiForm component is a modal form which is rendered dynamically, + * based on an API endpoint. + + */ +export function ApiForm(props: ApiFormProps) { // Form state const form = useForm({}); @@ -300,8 +295,8 @@ export function ApiForm({ // Query manager for retrieving form definition from the server const definitionQuery = useQuery({ - enabled: opened && !!url, - queryKey: ['form-definition', name, url, pk], + enabled: props.opened && !!props.url, + queryKey: ['form-definition', name, props.url, props.pk], queryFn: async () => { // Clear form construction error field setError(''); @@ -320,8 +315,12 @@ export function ApiForm({ // Query manager for retrieiving initial data from the server const initialDataQuery = useQuery({ - enabled: fetchInitialData && opened && !!url && fieldDefinitions.length > 0, - queryKey: ['form-initial-data', name, url, pk], + enabled: + props.fetchInitialData && + props.opened && + !!props.url && + fieldDefinitions.length > 0, + queryKey: ['form-initial-data', name, props.url, props.pk], queryFn: async () => { return api .get(getUrl()) @@ -357,10 +356,14 @@ export function ApiForm({ // Construct a fully-qualified URL based on the provided details function getUrl(): string { - let u = url; + if (!props.url) { + return ''; + } - if (pk && pk > 0) { - u += `${pk}/`; + let u = props.url; + + if (props.pk && props.pk > 0) { + u += `${props.pk}/`; } return u; @@ -374,10 +377,14 @@ export function ApiForm({ function extractFieldDefinitions( response: AxiosResponse ): ApiFormFieldType[] { - let actions = response.data?.actions[method.toUpperCase()] || []; + if (!props.method) { + return []; + } + + let actions = response.data?.actions[props.method.toUpperCase()] || []; if (actions.length == 0) { - setError(`Permission denied for ${method} at ${url}`); + setError(`Permission denied for ${props.method} at ${props.url}`); return []; } @@ -389,7 +396,7 @@ export function ApiForm({ name: fieldName, label: field.label, description: field.help_text, - value: field.value, + value: field.value || field.default, fieldType: field.type, required: field.required, placeholder: field.placeholder, @@ -405,11 +412,11 @@ export function ApiForm({ <Modal size="xl" radius="sm" - opened={opened} + opened={props.opened} onClose={() => { - onClose ? onClose() : null; + props.onClose ? props.onClose() : null; }} - title={title} + title={props.title} > <Stack> <Divider /> @@ -430,7 +437,7 @@ export function ApiForm({ {canRender && ( <ScrollArea> <Stack spacing="md"> - {fields.map((field) => ( + {props.fields.map((field) => ( <ApiFormField key={field.name} field={field} @@ -447,19 +454,24 @@ export function ApiForm({ </Stack> <Divider /> <Group position="right"> - <Button onClick={onClose} variant="outline" radius="sm" color="red"> - {cancelText} + <Button + onClick={props.onClose} + variant="outline" + radius="sm" + color={props.cancelColor ?? 'blue'} + > + {props.cancelText ?? `Cancel`} </Button> <Button onClick={() => null} variant="outline" radius="sm" - color="green" + color={props.submitColor ?? 'green'} disabled={!canSubmit} > <Group position="right" spacing={5} noWrap={true}> <Loader size="xs" /> - {submitText} + {props.submitText ?? `Submit`} </Group> </Button> </Group> diff --git a/src/frontend/src/components/forms/CreateApiForm.tsx b/src/frontend/src/components/forms/CreateApiForm.tsx new file mode 100644 index 0000000000..c19d8f6318 --- /dev/null +++ b/src/frontend/src/components/forms/CreateApiForm.tsx @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; + +import { ApiForm, ApiFormProps } from './ApiForm'; + +/** + * A modal form for creating a new database object via the API + */ +export function CreateApiForm(props: ApiFormProps) { + const createProps: ApiFormProps = useMemo(() => { + return { + ...props, + method: 'POST', + fetchInitialData: false + }; + }, [props]); + + return <ApiForm {...createProps} />; +} diff --git a/src/frontend/src/components/forms/DeleteApiForm.tsx b/src/frontend/src/components/forms/DeleteApiForm.tsx new file mode 100644 index 0000000000..15d54168f9 --- /dev/null +++ b/src/frontend/src/components/forms/DeleteApiForm.tsx @@ -0,0 +1,21 @@ +import { t } from '@lingui/macro'; +import { useMemo } from 'react'; + +import { ApiForm, ApiFormProps } from './ApiForm'; + +/** + * A modal form for deleting a single database object via the API. + */ +export function DeleteApiForm(props: ApiFormProps) { + const deleteProps: ApiFormProps = useMemo(() => { + return { + ...props, + method: 'DELETE', + fetchInitialData: false, + submitText: props.submitText ? props.submitText : t`Delete`, + submitColor: 'red' + }; + }, [props]); + + return <ApiForm {...deleteProps} />; +} diff --git a/src/frontend/src/components/forms/EditApiForm.tsx b/src/frontend/src/components/forms/EditApiForm.tsx new file mode 100644 index 0000000000..cb1ca207e2 --- /dev/null +++ b/src/frontend/src/components/forms/EditApiForm.tsx @@ -0,0 +1,19 @@ +import { useMemo } from 'react'; + +import { ApiForm, ApiFormProps } from './ApiForm'; + +/** + * A modal form for editing a single database object via the API. + */ +export function EditApiForm(props: ApiFormProps) { + const editProps: ApiFormProps = useMemo(() => { + return { + ...props, + method: 'PUT', + fetchInitialData: true, + submitText: props.submitText ? props.submitText : 'Save' + }; + }, [props]); + + return <ApiForm {...editProps} />; +} diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index a2f0233e71..b2a1a2a659 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -11,6 +11,9 @@ import { import { useState } from 'react'; import { ApiForm, ApiFormFieldType } from '../../components/forms/ApiForm'; +import { CreateApiForm } from '../../components/forms/CreateApiForm'; +import { DeleteApiForm } from '../../components/forms/DeleteApiForm'; +import { EditApiForm } from '../../components/forms/EditApiForm'; import { PlaceholderPill } from '../../components/items/Placeholder'; import { StylishText } from '../../components/items/StylishText'; @@ -18,6 +21,8 @@ export default function Home() { const [partFormOpened, setPartFormOpened] = useState(false); const [poFormOpened, setPoFormOpened] = useState(false); const [companyFormOpened, setCompanyFormOpened] = useState(false); + const [stockFormOpened, setStockFormOpened] = useState(false); + const [salesOrderFormOpened, setSalesOrderFormOpened] = useState(false); const partFields: ApiFormFieldType[] = [ { @@ -59,6 +64,18 @@ export default function Home() { } ]; + const salesOrderFields: ApiFormFieldType[] = [ + { + name: 'reference' + }, + { + name: 'customer' + }, + { + name: 'description' + } + ]; + const companyFields: ApiFormFieldType[] = [ { name: 'name' @@ -91,39 +108,51 @@ export default function Home() { </StylishText> <PlaceholderPill /> </Group> - <ApiForm + <EditApiForm name="part-edit" url="/part/" pk={1} fields={partFields} - method="PUT" title="Edit Part" opened={partFormOpened} onClose={() => setPartFormOpened(false)} - fetchInitialData={true} /> - <ApiForm + <EditApiForm name="po-edit" url="/order/po/" pk={1} fields={poFields} - method="PUT" title="Edit Purchase Order" opened={poFormOpened} onClose={() => setPoFormOpened(false)} - fetchInitialData={true} /> - <ApiForm + <EditApiForm name="company-edit" url="/company/" pk={1} fields={companyFields} - method="PUT" title="Edit Company" opened={companyFormOpened} onClose={() => setCompanyFormOpened(false)} - fetchInitialData={true} /> + <DeleteApiForm + name="stock-delete" + url="/stock/" + title="Delete Stock Item" + pk={1} + fields={[]} + opened={stockFormOpened} + onClose={() => setStockFormOpened(false)} + /> + <CreateApiForm + name="sales-order-create" + url="/order/so/" + title="Create Sales Order" + fields={salesOrderFields} + opened={salesOrderFormOpened} + onClose={() => setSalesOrderFormOpened(false)} + /> + <Stack align="flex-start" spacing="xs"> <Button onClick={() => setPartFormOpened(true)} @@ -146,6 +175,20 @@ export default function Home() { > Edit Company Form </Button> + <Button + variant="outline" + color="green" + onClick={() => setSalesOrderFormOpened(true)} + > + Create Sales Order Form + </Button> + <Button + variant="outline" + color="red" + onClick={() => setStockFormOpened(true)} + > + Delete Stock Item Form + </Button> </Stack> </> );