-
-
Notifications
You must be signed in to change notification settings - Fork 760
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Very basic form implementation * Fetch field definition data via AP * Add cancel and submit buttons * Render basic field stack, and extract field data from API * Extract specific field definition * Handle text fields * Add some more fields * Implement boolean and number fields * Add callback for value changes * Use form state to update values * Add skeleton for a 'related field' * Framework for related field query manager * Handle date type fields * Make date input clearable * Fix error messae * Fix for optional callback function * Use LoadingOverlay component * Support url and email fields * Add icon support - Cannot hash react nodes! * Create components for different form types - Create - Edit - Delete * Split ApiFormField into separate file * Add support for pre-form and post-form content * Don't render hidden fields * Smaller spacing * More demo data * Add icon to clear text input value * Account for "read only" property * Framework for a submit data query * Return 404 on API requests other than GET - Other request methods need love too! * Starting work on dynamically opening forms * Check validity of OPTIONS response * refactor * Launch modal form with provided props * Refactor tractor: - Handle simple form submission - Handle simple error messages * Improve support for content pre and post form * Allow custom content to be inserted between fields * Pass form props down to individual fields * Update playground page with API forms functionality * Simplify form submission to handle different methods * Handle passing of initial form data values * Improve docstrings * Code cleanup and add translations * Add comment * Ignore icon for checkbox input * Add custom callback function for individual form fields * Use Switch instead of Checkbox * Add react-select * Implement very simple related field select input - No custom rendering yet - Simple pk / name combination * FIrst pass at retrieving data from API * Updates: - Implement "filters" for each form field - Prevent duplicate searches from doing weird things * Rearrange files * Load initial values for related fields from the API - Requires cleanup * Display error message for related field * Create some basic functions for construction field sets * Display non-field-errors in form * Improved error rendering * Change field definition from list to Record type - In line with current (javascript) implementation - Cleaner / simpler to work with * Correctly use default values on first form load * Improve date input * define a set of stockitem fields * Implement "Choice" field using mantine.select * Implement useForm hook for better performance * Show permission denied error * Improved callback "onChangeValue" functionality - Define proper return type - Access all form data * Cleanup * Implement components for rendering database model instance - Not fully featured yet (still a lot of work to go) - Porting code across from existing "model_renderers.js" * Update packages * Handle file input fields * Improved loading overlay for form submission * Utilize modal renderers in search results * SearchDrawer cleanup * Temporary fix for image pathing issue * Cleanup table action buttons - Now use a dropdown menu - Implement "edit part" directly from the table - This is only as an example for now * Fix playground * Generate random ID with useId hook * Fix abortController to use ref * Use AbortController for search panel * Fix TableColumn type definition * Improved generation of unique form ID values
- Loading branch information
1 parent
1e55fc8
commit baa9f36
Showing
26 changed files
with
1,976 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
import { t } from '@lingui/macro'; | ||
import { | ||
Alert, | ||
Divider, | ||
LoadingOverlay, | ||
ScrollArea, | ||
Text | ||
} from '@mantine/core'; | ||
import { Button, Group, Stack } from '@mantine/core'; | ||
import { useForm } from '@mantine/form'; | ||
import { modals } from '@mantine/modals'; | ||
import { notifications } from '@mantine/notifications'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { useEffect, useMemo } from 'react'; | ||
import { useState } from 'react'; | ||
|
||
import { api } from '../../App'; | ||
import { constructFormUrl } from '../../functions/forms'; | ||
import { invalidResponse } from '../../functions/notifications'; | ||
import { | ||
ApiFormField, | ||
ApiFormFieldSet, | ||
ApiFormFieldType | ||
} from './fields/ApiFormField'; | ||
|
||
/** | ||
* Properties for the ApiForm component | ||
* @param name : The name (identifier) for this form | ||
* @param url : The API endpoint to fetch the form data from | ||
* @param pk : Optional primary-key value when editing an existing object | ||
* @param title : The title to display in the form header | ||
* @param fields : The fields to render in the form | ||
* @param submitText : Optional custom text to display on the submit button (default: Submit)4 | ||
* @param submitColor : Optional custom color for the submit button (default: green) | ||
* @param cancelText : Optional custom text to display on the cancel button (default: Cancel) | ||
* @param cancelColor : Optional custom color for the cancel button (default: blue) | ||
* @param fetchInitialData : Optional flag to fetch initial data from the server (default: true) | ||
* @param method : Optional HTTP method to use when submitting the form (default: GET) | ||
* @param preFormContent : Optional content to render before the form fields | ||
* @param postFormContent : Optional content to render after the form fields | ||
* @param successMessage : Optional message to display on successful form submission | ||
* @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: ApiFormFieldSet; | ||
cancelText?: string; | ||
submitText?: string; | ||
submitColor?: string; | ||
cancelColor?: string; | ||
fetchInitialData?: boolean; | ||
method?: string; | ||
preFormContent?: JSX.Element | (() => JSX.Element); | ||
postFormContent?: JSX.Element | (() => JSX.Element); | ||
successMessage?: string; | ||
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({ | ||
modalId, | ||
props, | ||
fieldDefinitions | ||
}: { | ||
modalId: string; | ||
props: ApiFormProps; | ||
fieldDefinitions: ApiFormFieldSet; | ||
}) { | ||
// Form errors which are not associated with a specific field | ||
const [nonFieldErrors, setNonFieldErrors] = useState<string[]>([]); | ||
|
||
// Form state | ||
const form = useForm({}); | ||
|
||
// Cache URL | ||
const url = useMemo(() => constructFormUrl(props), [props]); | ||
|
||
// Render pre-form content | ||
// TODO: Future work will allow this content to be updated dynamically based on the form data | ||
const preFormElement: JSX.Element | null = useMemo(() => { | ||
if (props.preFormContent === undefined) { | ||
return null; | ||
} else if (props.preFormContent instanceof Function) { | ||
return props.preFormContent(); | ||
} else { | ||
return props.preFormContent; | ||
} | ||
}, [props]); | ||
|
||
// Render post-form content | ||
// TODO: Future work will allow this content to be updated dynamically based on the form data | ||
const postFormElement: JSX.Element | null = useMemo(() => { | ||
if (props.postFormContent === undefined) { | ||
return null; | ||
} else if (props.postFormContent instanceof Function) { | ||
return props.postFormContent(); | ||
} else { | ||
return props.postFormContent; | ||
} | ||
}, [props]); | ||
|
||
// Query manager for retrieiving initial data from the server | ||
const initialDataQuery = useQuery({ | ||
enabled: false, | ||
queryKey: ['form-initial-data', props.name, props.url, props.pk], | ||
queryFn: async () => { | ||
return api | ||
.get(url) | ||
.then((response) => { | ||
// Update form values, but only for the fields specified for the form | ||
Object.keys(props.fields).forEach((fieldName) => { | ||
if (fieldName in response.data) { | ||
form.setValues({ | ||
[fieldName]: response.data[fieldName] | ||
}); | ||
} | ||
}); | ||
|
||
return response; | ||
}) | ||
.catch((error) => { | ||
console.error('Error fetching initial data:', error); | ||
}); | ||
} | ||
}); | ||
|
||
// Fetch initial data on form load | ||
useEffect(() => { | ||
// Provide initial form data | ||
Object.entries(props.fields).forEach(([fieldName, field]) => { | ||
if (field.value !== undefined) { | ||
form.setValues({ | ||
[fieldName]: field.value | ||
}); | ||
} else if (field.default !== undefined) { | ||
form.setValues({ | ||
[fieldName]: field.default | ||
}); | ||
} | ||
}); | ||
|
||
// Fetch initial data if the fetchInitialData property is set | ||
if (props.fetchInitialData) { | ||
initialDataQuery.refetch(); | ||
} | ||
}, []); | ||
|
||
// Query manager for submitting data | ||
const submitQuery = useQuery({ | ||
enabled: false, | ||
queryKey: ['form-submit', props.name, props.url, props.pk], | ||
queryFn: async () => { | ||
let method = props.method?.toLowerCase() ?? 'get'; | ||
|
||
api({ | ||
method: method, | ||
url: url, | ||
data: form.values, | ||
headers: { | ||
'Content-Type': 'multipart/form-data' | ||
} | ||
}) | ||
.then((response) => { | ||
switch (response.status) { | ||
case 200: | ||
case 201: | ||
case 204: | ||
// Form was submitted successfully | ||
|
||
// Optionally call the onFormSuccess callback | ||
if (props.onFormSuccess) { | ||
props.onFormSuccess(); | ||
} | ||
|
||
// Optionally show a success message | ||
if (props.successMessage) { | ||
notifications.show({ | ||
title: t`Success`, | ||
message: props.successMessage, | ||
color: 'green' | ||
}); | ||
} | ||
|
||
closeForm(); | ||
break; | ||
default: | ||
// Unexpected state on form success | ||
invalidResponse(response.status); | ||
closeForm(); | ||
break; | ||
} | ||
}) | ||
.catch((error) => { | ||
if (error.response) { | ||
switch (error.response.status) { | ||
case 400: | ||
// Data validation error | ||
form.setErrors(error.response.data); | ||
setNonFieldErrors(error.response.data.non_field_errors ?? []); | ||
break; | ||
default: | ||
// Unexpected state on form error | ||
invalidResponse(error.response.status); | ||
closeForm(); | ||
break; | ||
} | ||
} else { | ||
invalidResponse(0); | ||
closeForm(); | ||
} | ||
|
||
return error; | ||
}); | ||
}, | ||
refetchOnMount: false, | ||
refetchOnWindowFocus: false | ||
}); | ||
|
||
// Data loading state | ||
const [isLoading, setIsLoading] = useState<boolean>(true); | ||
|
||
useEffect(() => { | ||
setIsLoading(submitQuery.isFetching || initialDataQuery.isFetching); | ||
}, [initialDataQuery.status, submitQuery.status]); | ||
|
||
/** | ||
* Callback to perform form submission | ||
*/ | ||
function submitForm() { | ||
setIsLoading(true); | ||
submitQuery.refetch(); | ||
} | ||
|
||
/** | ||
* Callback to close the form | ||
* Note that the calling function might implement an onClose() callback, | ||
* which will be automatically called | ||
*/ | ||
function closeForm() { | ||
modals.close(modalId); | ||
} | ||
|
||
return ( | ||
<Stack> | ||
<Divider /> | ||
<Stack spacing="sm"> | ||
<LoadingOverlay visible={isLoading} /> | ||
{(Object.keys(form.errors).length > 0 || nonFieldErrors.length > 0) && ( | ||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}> | ||
{nonFieldErrors.length > 0 && ( | ||
<Stack spacing="xs"> | ||
{nonFieldErrors.map((message) => ( | ||
<Text key={message}>{message}</Text> | ||
))} | ||
</Stack> | ||
)} | ||
</Alert> | ||
)} | ||
{preFormElement} | ||
<ScrollArea> | ||
<Stack spacing="xs"> | ||
{Object.entries(props.fields).map( | ||
([fieldName, field]) => | ||
!field.hidden && ( | ||
<ApiFormField | ||
key={fieldName} | ||
field={field} | ||
fieldName={fieldName} | ||
formProps={props} | ||
form={form} | ||
error={form.errors[fieldName] ?? null} | ||
definitions={fieldDefinitions} | ||
/> | ||
) | ||
)} | ||
</Stack> | ||
</ScrollArea> | ||
{postFormElement} | ||
</Stack> | ||
<Divider /> | ||
<Group position="right"> | ||
<Button | ||
onClick={closeForm} | ||
variant="outline" | ||
radius="sm" | ||
color={props.cancelColor ?? 'blue'} | ||
> | ||
{props.cancelText ?? t`Cancel`} | ||
</Button> | ||
<Button | ||
onClick={submitForm} | ||
variant="outline" | ||
radius="sm" | ||
color={props.submitColor ?? 'green'} | ||
disabled={isLoading} | ||
> | ||
{props.submitText ?? t`Submit`} | ||
</Button> | ||
</Group> | ||
</Stack> | ||
); | ||
} |
Oops, something went wrong.