mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 19:45:46 +00:00
Create components for different form types
- Create - Edit - Delete
This commit is contained in:
@ -32,6 +32,7 @@ export type ApiFormFieldType = {
|
|||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
default?: any;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
fieldType?: string;
|
fieldType?: string;
|
||||||
api_url?: string;
|
api_url?: string;
|
||||||
@ -249,8 +250,7 @@ function ApiFormField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ApiForm component is a modal form which is rendered dynamically,
|
* Properties for the ApiForm component
|
||||||
* based on an API endpoint.
|
|
||||||
* @param url : The API endpoint to fetch the form from.
|
* @param url : The API endpoint to fetch the form from.
|
||||||
* @param fields : The fields to render in the form.
|
* @param fields : The fields to render in the form.
|
||||||
* @param opened : Whether the form is opened or not.
|
* @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 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.
|
* @param onFormError : A callback function to call when the form is submitted with errors.
|
||||||
*/
|
*/
|
||||||
export function ApiForm({
|
export interface ApiFormProps {
|
||||||
name,
|
|
||||||
url,
|
|
||||||
pk,
|
|
||||||
title,
|
|
||||||
fields,
|
|
||||||
opened,
|
|
||||||
onClose,
|
|
||||||
onFormSuccess,
|
|
||||||
onFormError,
|
|
||||||
cancelText = t`Cancel`,
|
|
||||||
submitText = t`Submit`,
|
|
||||||
method = 'PUT',
|
|
||||||
fetchInitialData = false
|
|
||||||
}: {
|
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
pk?: number;
|
pk?: number;
|
||||||
@ -280,13 +266,22 @@ export function ApiForm({
|
|||||||
fields: ApiFormFieldType[];
|
fields: ApiFormFieldType[];
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
submitText?: string;
|
submitText?: string;
|
||||||
|
submitColor?: string;
|
||||||
|
cancelColor?: string;
|
||||||
fetchInitialData?: boolean;
|
fetchInitialData?: boolean;
|
||||||
method?: string;
|
method?: string;
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onFormSuccess?: () => void;
|
onFormSuccess?: () => void;
|
||||||
onFormError?: () => 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
|
// Form state
|
||||||
const form = useForm({});
|
const form = useForm({});
|
||||||
|
|
||||||
@ -300,8 +295,8 @@ export function ApiForm({
|
|||||||
|
|
||||||
// Query manager for retrieving form definition from the server
|
// Query manager for retrieving form definition from the server
|
||||||
const definitionQuery = useQuery({
|
const definitionQuery = useQuery({
|
||||||
enabled: opened && !!url,
|
enabled: props.opened && !!props.url,
|
||||||
queryKey: ['form-definition', name, url, pk],
|
queryKey: ['form-definition', name, props.url, props.pk],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// Clear form construction error field
|
// Clear form construction error field
|
||||||
setError('');
|
setError('');
|
||||||
@ -320,8 +315,12 @@ export function ApiForm({
|
|||||||
|
|
||||||
// Query manager for retrieiving initial data from the server
|
// Query manager for retrieiving initial data from the server
|
||||||
const initialDataQuery = useQuery({
|
const initialDataQuery = useQuery({
|
||||||
enabled: fetchInitialData && opened && !!url && fieldDefinitions.length > 0,
|
enabled:
|
||||||
queryKey: ['form-initial-data', name, url, pk],
|
props.fetchInitialData &&
|
||||||
|
props.opened &&
|
||||||
|
!!props.url &&
|
||||||
|
fieldDefinitions.length > 0,
|
||||||
|
queryKey: ['form-initial-data', name, props.url, props.pk],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return api
|
return api
|
||||||
.get(getUrl())
|
.get(getUrl())
|
||||||
@ -357,10 +356,14 @@ export function ApiForm({
|
|||||||
|
|
||||||
// Construct a fully-qualified URL based on the provided details
|
// Construct a fully-qualified URL based on the provided details
|
||||||
function getUrl(): string {
|
function getUrl(): string {
|
||||||
let u = url;
|
if (!props.url) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (pk && pk > 0) {
|
let u = props.url;
|
||||||
u += `${pk}/`;
|
|
||||||
|
if (props.pk && props.pk > 0) {
|
||||||
|
u += `${props.pk}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return u;
|
return u;
|
||||||
@ -374,10 +377,14 @@ export function ApiForm({
|
|||||||
function extractFieldDefinitions(
|
function extractFieldDefinitions(
|
||||||
response: AxiosResponse
|
response: AxiosResponse
|
||||||
): ApiFormFieldType[] {
|
): ApiFormFieldType[] {
|
||||||
let actions = response.data?.actions[method.toUpperCase()] || [];
|
if (!props.method) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let actions = response.data?.actions[props.method.toUpperCase()] || [];
|
||||||
|
|
||||||
if (actions.length == 0) {
|
if (actions.length == 0) {
|
||||||
setError(`Permission denied for ${method} at ${url}`);
|
setError(`Permission denied for ${props.method} at ${props.url}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +396,7 @@ export function ApiForm({
|
|||||||
name: fieldName,
|
name: fieldName,
|
||||||
label: field.label,
|
label: field.label,
|
||||||
description: field.help_text,
|
description: field.help_text,
|
||||||
value: field.value,
|
value: field.value || field.default,
|
||||||
fieldType: field.type,
|
fieldType: field.type,
|
||||||
required: field.required,
|
required: field.required,
|
||||||
placeholder: field.placeholder,
|
placeholder: field.placeholder,
|
||||||
@ -405,11 +412,11 @@ export function ApiForm({
|
|||||||
<Modal
|
<Modal
|
||||||
size="xl"
|
size="xl"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
opened={opened}
|
opened={props.opened}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onClose ? onClose() : null;
|
props.onClose ? props.onClose() : null;
|
||||||
}}
|
}}
|
||||||
title={title}
|
title={props.title}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -430,7 +437,7 @@ export function ApiForm({
|
|||||||
{canRender && (
|
{canRender && (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
{fields.map((field) => (
|
{props.fields.map((field) => (
|
||||||
<ApiFormField
|
<ApiFormField
|
||||||
key={field.name}
|
key={field.name}
|
||||||
field={field}
|
field={field}
|
||||||
@ -447,19 +454,24 @@ export function ApiForm({
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button onClick={onClose} variant="outline" radius="sm" color="red">
|
<Button
|
||||||
{cancelText}
|
onClick={props.onClose}
|
||||||
|
variant="outline"
|
||||||
|
radius="sm"
|
||||||
|
color={props.cancelColor ?? 'blue'}
|
||||||
|
>
|
||||||
|
{props.cancelText ?? `Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => null}
|
onClick={() => null}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
color="green"
|
color={props.submitColor ?? 'green'}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
>
|
>
|
||||||
<Group position="right" spacing={5} noWrap={true}>
|
<Group position="right" spacing={5} noWrap={true}>
|
||||||
<Loader size="xs" />
|
<Loader size="xs" />
|
||||||
{submitText}
|
{props.submitText ?? `Submit`}
|
||||||
</Group>
|
</Group>
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
18
src/frontend/src/components/forms/CreateApiForm.tsx
Normal file
18
src/frontend/src/components/forms/CreateApiForm.tsx
Normal file
@ -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} />;
|
||||||
|
}
|
21
src/frontend/src/components/forms/DeleteApiForm.tsx
Normal file
21
src/frontend/src/components/forms/DeleteApiForm.tsx
Normal file
@ -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} />;
|
||||||
|
}
|
19
src/frontend/src/components/forms/EditApiForm.tsx
Normal file
19
src/frontend/src/components/forms/EditApiForm.tsx
Normal file
@ -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} />;
|
||||||
|
}
|
@ -11,6 +11,9 @@ import {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ApiForm, ApiFormFieldType } from '../../components/forms/ApiForm';
|
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 { PlaceholderPill } from '../../components/items/Placeholder';
|
||||||
import { StylishText } from '../../components/items/StylishText';
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
|
|
||||||
@ -18,6 +21,8 @@ export default function Home() {
|
|||||||
const [partFormOpened, setPartFormOpened] = useState(false);
|
const [partFormOpened, setPartFormOpened] = useState(false);
|
||||||
const [poFormOpened, setPoFormOpened] = useState(false);
|
const [poFormOpened, setPoFormOpened] = useState(false);
|
||||||
const [companyFormOpened, setCompanyFormOpened] = useState(false);
|
const [companyFormOpened, setCompanyFormOpened] = useState(false);
|
||||||
|
const [stockFormOpened, setStockFormOpened] = useState(false);
|
||||||
|
const [salesOrderFormOpened, setSalesOrderFormOpened] = useState(false);
|
||||||
|
|
||||||
const partFields: ApiFormFieldType[] = [
|
const partFields: ApiFormFieldType[] = [
|
||||||
{
|
{
|
||||||
@ -59,6 +64,18 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const salesOrderFields: ApiFormFieldType[] = [
|
||||||
|
{
|
||||||
|
name: 'reference'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const companyFields: ApiFormFieldType[] = [
|
const companyFields: ApiFormFieldType[] = [
|
||||||
{
|
{
|
||||||
name: 'name'
|
name: 'name'
|
||||||
@ -91,39 +108,51 @@ export default function Home() {
|
|||||||
</StylishText>
|
</StylishText>
|
||||||
<PlaceholderPill />
|
<PlaceholderPill />
|
||||||
</Group>
|
</Group>
|
||||||
<ApiForm
|
<EditApiForm
|
||||||
name="part-edit"
|
name="part-edit"
|
||||||
url="/part/"
|
url="/part/"
|
||||||
pk={1}
|
pk={1}
|
||||||
fields={partFields}
|
fields={partFields}
|
||||||
method="PUT"
|
|
||||||
title="Edit Part"
|
title="Edit Part"
|
||||||
opened={partFormOpened}
|
opened={partFormOpened}
|
||||||
onClose={() => setPartFormOpened(false)}
|
onClose={() => setPartFormOpened(false)}
|
||||||
fetchInitialData={true}
|
|
||||||
/>
|
/>
|
||||||
<ApiForm
|
<EditApiForm
|
||||||
name="po-edit"
|
name="po-edit"
|
||||||
url="/order/po/"
|
url="/order/po/"
|
||||||
pk={1}
|
pk={1}
|
||||||
fields={poFields}
|
fields={poFields}
|
||||||
method="PUT"
|
|
||||||
title="Edit Purchase Order"
|
title="Edit Purchase Order"
|
||||||
opened={poFormOpened}
|
opened={poFormOpened}
|
||||||
onClose={() => setPoFormOpened(false)}
|
onClose={() => setPoFormOpened(false)}
|
||||||
fetchInitialData={true}
|
|
||||||
/>
|
/>
|
||||||
<ApiForm
|
<EditApiForm
|
||||||
name="company-edit"
|
name="company-edit"
|
||||||
url="/company/"
|
url="/company/"
|
||||||
pk={1}
|
pk={1}
|
||||||
fields={companyFields}
|
fields={companyFields}
|
||||||
method="PUT"
|
|
||||||
title="Edit Company"
|
title="Edit Company"
|
||||||
opened={companyFormOpened}
|
opened={companyFormOpened}
|
||||||
onClose={() => setCompanyFormOpened(false)}
|
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">
|
<Stack align="flex-start" spacing="xs">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setPartFormOpened(true)}
|
onClick={() => setPartFormOpened(true)}
|
||||||
@ -146,6 +175,20 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
Edit Company Form
|
Edit Company Form
|
||||||
</Button>
|
</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>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user