2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 19:45:46 +00:00
Files
InvenTree/src/frontend/src/components/forms/ApiForm.tsx
2023-07-27 22:05:20 +10:00

264 lines
7.3 KiB
TypeScript

import {
Alert,
Divider,
LoadingOverlay,
Modal,
ScrollArea
} from '@mantine/core';
import { Button, Group, Loader, Stack } 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';
import { useState } from 'react';
import { api } from '../../App';
import { ApiFormField, ApiFormFieldType } from './ApiFormField';
/**
* 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.
* @param onClose : A callback function to call when the form is closed.
* @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 interface ApiFormProps {
name: string;
url: string;
pk?: number;
title: string;
fields: ApiFormFieldType[];
cancelText?: string;
submitText?: string;
submitColor?: string;
cancelColor?: string;
fetchInitialData?: boolean;
method?: string;
opened: boolean;
preFormContent?: JSX.Element;
preFormContentFunc?: () => JSX.Element;
postFormContent?: JSX.Element;
postFormContentFunc?: () => JSX.Element;
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({});
// Form field definitions (via API)
const [fieldDefinitions, setFieldDefinitions] = useState<ApiFormFieldType[]>(
[]
);
// Error observed during form construction
const [error, setError] = useState<string>('');
// Query manager for retrieving form definition from the server
const definitionQuery = useQuery({
enabled: props.opened && !!props.url,
queryKey: ['form-definition', name, props.url, props.pk],
queryFn: async () => {
// Clear form construction error field
setError('');
return api
.options(getUrl())
.then((response) => {
setFieldDefinitions(extractFieldDefinitions(response));
return response;
})
.catch((error) => {
setError(error.message);
setFieldDefinitions([]);
});
}
});
// Query manager for retrieiving initial data from the server
const initialDataQuery = useQuery({
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())
.then((response) => {
form.setValues(response.data);
return response;
})
.catch((error) => {
console.error('Error fetching initial data:', error);
setError(error.message);
});
}
});
// State variable to determine if the form can render the data
const [canRender, setCanRender] = useState<boolean>(false);
// Update the canRender state variable on status change
useEffect(() => {
setCanRender(
!definitionQuery.isFetching && definitionQuery.isSuccess && !error
);
}, [definitionQuery, error]);
// State variable to determine if the form can be submitted
const [canSubmit, setCanSubmit] = useState<boolean>(false);
// Update the canSubmit state variable on status change
useEffect(() => {
setCanSubmit(canRender && true);
// TODO: This will be updated when we have a query manager for form submission
}, [canRender]);
// Construct a fully-qualified URL based on the provided details
function getUrl(): string {
if (!props.url) {
return '';
}
let u = props.url;
if (props.pk && props.pk > 0) {
u += `${props.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[] {
if (!props.method) {
return [];
}
let actions = response.data?.actions[props.method.toUpperCase()] || [];
if (actions.length == 0) {
setError(`Permission denied for ${props.method} at ${props.url}`);
return [];
}
let definitions: ApiFormFieldType[] = [];
for (const fieldName in actions) {
const field = actions[fieldName];
definitions.push({
name: fieldName,
label: field.label,
description: field.help_text,
value: field.value || field.default,
fieldType: field.type,
required: field.required,
placeholder: field.placeholder,
api_url: field.api_url,
model: field.model,
read_only: field.read_only
});
}
return definitions;
}
return (
<Modal
size="xl"
radius="sm"
opened={props.opened}
overlayProps={{
blur: 1,
opacity: 0.5
}}
onClose={() => {
props.onClose ? props.onClose() : null;
}}
title={props.title}
>
<Stack>
<Divider />
<Stack spacing="sm">
<LoadingOverlay
visible={definitionQuery.isFetching || initialDataQuery.isFetching}
/>
{error && (
<Alert
radius="sm"
color="red"
title={`Error`}
icon={<IconAlertCircle size="1rem" />}
>
{error}
</Alert>
)}
{props.preFormContent && props.preFormContent}
{props.preFormContentFunc ? props.preFormContentFunc() : null}
{canRender && (
<ScrollArea>
<Stack spacing="xs">
{props.fields
.filter((field) => !field.hidden)
.map((field) => (
<ApiFormField
key={field.name}
field={field}
form={form}
definitions={fieldDefinitions}
onValueChange={(fieldName, value) => {
form.setValues({ [fieldName]: value });
}}
/>
))}
</Stack>
</ScrollArea>
)}
{props.postFormContent && props.postFormContent}
{props.postFormContentFunc ? props.postFormContentFunc() : null}
</Stack>
<Divider />
<Group position="right">
<Button
onClick={props.onClose}
variant="outline"
radius="sm"
color={props.cancelColor ?? 'blue'}
>
{props.cancelText ?? `Cancel`}
</Button>
<Button
onClick={() => null}
variant="outline"
radius="sm"
color={props.submitColor ?? 'green'}
disabled={!canSubmit}
>
<Group position="right" spacing={5} noWrap={true}>
<Loader size="xs" />
{props.submitText ?? `Submit`}
</Group>
</Button>
</Group>
</Stack>
</Modal>
);
}