mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	[ui] Enable form auto-fill (#10061)
* Enable form auto-fill - If a single value is available, pre-fill - Must be enabled per-field * Tweak playwright tests for increased reliability * Fix deps
This commit is contained in:
		| @@ -56,6 +56,7 @@ export type ApiFormFieldHeader = { | |||||||
|  * @param description : The description to display for the field |  * @param description : The description to display for the field | ||||||
|  * @param preFieldContent : Content to render before the field |  * @param preFieldContent : Content to render before the field | ||||||
|  * @param postFieldContent : Content to render after the field |  * @param postFieldContent : Content to render after the field | ||||||
|  |  * @param autoFill: Whether to automatically fill the field with data from the API | ||||||
|  * @param onValueChange : Callback function to call when the field value changes |  * @param onValueChange : Callback function to call when the field value changes | ||||||
|  * @param adjustFilters : Callback function to adjust the filters for a related field before a query is made |  * @param adjustFilters : Callback function to adjust the filters for a related field before a query is made | ||||||
|  * @param adjustValue : Callback function to adjust the value of the field before it is sent to the API |  * @param adjustValue : Callback function to adjust the value of the field before it is sent to the API | ||||||
| @@ -104,6 +105,7 @@ export type ApiFormFieldType = { | |||||||
|   description?: string; |   description?: string; | ||||||
|   preFieldContent?: JSX.Element; |   preFieldContent?: JSX.Element; | ||||||
|   postFieldContent?: JSX.Element; |   postFieldContent?: JSX.Element; | ||||||
|  |   autoFill?: boolean; | ||||||
|   adjustValue?: (value: any) => any; |   adjustValue?: (value: any) => any; | ||||||
|   onValueChange?: (value: any, record?: any) => void; |   onValueChange?: (value: any, record?: any) => void; | ||||||
|   adjustFilters?: (value: ApiFormAdjustFilterType) => any; |   adjustFilters?: (value: ApiFormAdjustFilterType) => any; | ||||||
|   | |||||||
| @@ -80,6 +80,7 @@ export function PrintingActions({ | |||||||
|     // Override field values |     // Override field values | ||||||
|     fields.template = { |     fields.template = { | ||||||
|       ...fields.template, |       ...fields.template, | ||||||
|  |       autoFill: true, | ||||||
|       filters: { |       filters: { | ||||||
|         enabled: true, |         enabled: true, | ||||||
|         model_type: modelType, |         model_type: modelType, | ||||||
| @@ -132,6 +133,7 @@ export function PrintingActions({ | |||||||
|     timeout: 5000, |     timeout: 5000, | ||||||
|     fields: { |     fields: { | ||||||
|       template: { |       template: { | ||||||
|  |         autoFill: true, | ||||||
|         filters: { |         filters: { | ||||||
|           enabled: true, |           enabled: true, | ||||||
|           model_type: modelType, |           model_type: modelType, | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ export function ApiFormField({ | |||||||
|   const reducedDefinition = useMemo(() => { |   const reducedDefinition = useMemo(() => { | ||||||
|     return { |     return { | ||||||
|       ...fieldDefinition, |       ...fieldDefinition, | ||||||
|  |       autoFill: undefined, | ||||||
|       onValueChange: undefined, |       onValueChange: undefined, | ||||||
|       adjustFilters: undefined, |       adjustFilters: undefined, | ||||||
|       adjustValue: undefined, |       adjustValue: undefined, | ||||||
|   | |||||||
| @@ -54,6 +54,61 @@ export function RelatedModelField({ | |||||||
|  |  | ||||||
|   const [isOpen, setIsOpen] = useState<boolean>(false); |   const [isOpen, setIsOpen] = useState<boolean>(false); | ||||||
|  |  | ||||||
|  |   // Auto-fill the field with data from the API | ||||||
|  |   useEffect(() => { | ||||||
|  |     // If there is *no value defined*, and autoFill is enabled, then fetch data from the API | ||||||
|  |     if (!definition.autoFill || !definition.api_url) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (field.value != undefined) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const params = definition?.filters ?? {}; | ||||||
|  |  | ||||||
|  |     api | ||||||
|  |       .get(definition.api_url, { | ||||||
|  |         params: { | ||||||
|  |           ...params, | ||||||
|  |           limit: 1, | ||||||
|  |           offset: 0 | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       .then((response) => { | ||||||
|  |         const data: any = response?.data ?? {}; | ||||||
|  |  | ||||||
|  |         if (data.count === 1 && data.results?.length === 1) { | ||||||
|  |           // If there is only a single result, set the field value to that result | ||||||
|  |           const pk_field = definition.pk_field ?? 'pk'; | ||||||
|  |           if (data.results[0][pk_field]) { | ||||||
|  |             const value = { | ||||||
|  |               value: data.results[0][pk_field], | ||||||
|  |               data: data.results[0] | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             // Run custom callback for this field (if provided) | ||||||
|  |             if (definition.onValueChange) { | ||||||
|  |               definition.onValueChange( | ||||||
|  |                 data.results[0][pk_field], | ||||||
|  |                 data.results[0] | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             setInitialData(value); | ||||||
|  |             dataRef.current = [value]; | ||||||
|  |             setPk(data.results[0][pk_field]); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |   }, [ | ||||||
|  |     definition.autoFill, | ||||||
|  |     definition.api_url, | ||||||
|  |     definition.filters, | ||||||
|  |     definition.pk_field, | ||||||
|  |     field.value | ||||||
|  |   ]); | ||||||
|  |  | ||||||
|   // If an initial value is provided, load from the API |   // If an initial value is provided, load from the API | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     // If the value is unchanged, do nothing |     // If the value is unchanged, do nothing | ||||||
| @@ -98,7 +153,12 @@ export function RelatedModelField({ | |||||||
|     } else { |     } else { | ||||||
|       setPk(null); |       setPk(null); | ||||||
|     } |     } | ||||||
|   }, [definition.api_url, definition.pk_field, field.value]); |   }, [ | ||||||
|  |     definition.api_url, | ||||||
|  |     definition.filters, | ||||||
|  |     definition.pk_field, | ||||||
|  |     field.value | ||||||
|  |   ]); | ||||||
|  |  | ||||||
|   // Search input query |   // Search input query | ||||||
|   const [value, setValue] = useState<string>(''); |   const [value, setValue] = useState<string>(''); | ||||||
| @@ -221,6 +281,7 @@ export function RelatedModelField({ | |||||||
|   const fieldDefinition = useMemo(() => { |   const fieldDefinition = useMemo(() => { | ||||||
|     return { |     return { | ||||||
|       ...definition, |       ...definition, | ||||||
|  |       autoFill: undefined, | ||||||
|       onValueChange: undefined, |       onValueChange: undefined, | ||||||
|       adjustFilters: undefined, |       adjustFilters: undefined, | ||||||
|       exclude: undefined, |       exclude: undefined, | ||||||
|   | |||||||
| @@ -32,13 +32,12 @@ test('Label Printing', async ({ browser }) => { | |||||||
|  |  | ||||||
|   // Select label template |   // Select label template | ||||||
|   await page.getByLabel('related-field-template').click(); |   await page.getByLabel('related-field-template').click(); | ||||||
|   await page.getByText('InvenTree Stock Item Label (').click(); |   await page | ||||||
|  |     .getByRole('option', { name: 'InvenTree Stock Item Label' }) | ||||||
|   await page.waitForTimeout(100); |     .click(); | ||||||
|  |  | ||||||
|   await page.getByLabel('related-field-plugin').click(); |   await page.getByLabel('related-field-plugin').click(); | ||||||
|  |   await page.getByRole('option', { name: 'InvenTreeLabel provides' }).click(); | ||||||
|   await page.getByText('InvenTreeLabel', { exact: true }).click(); |  | ||||||
|  |  | ||||||
|   // Submit the print form (second time should result in success) |   // Submit the print form (second time should result in success) | ||||||
|   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); |   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); | ||||||
| @@ -71,9 +70,7 @@ test('Report Printing', async ({ browser }) => { | |||||||
|  |  | ||||||
|   // Select template |   // Select template | ||||||
|   await page.getByLabel('related-field-template').click(); |   await page.getByLabel('related-field-template').click(); | ||||||
|   await page.getByText('InvenTree Purchase Order').click(); |   await page.getByRole('option', { name: 'InvenTree Purchase Order' }).click(); | ||||||
|  |  | ||||||
|   await page.waitForTimeout(100); |  | ||||||
|  |  | ||||||
|   // Submit the print form (should result in success) |   // Submit the print form (should result in success) | ||||||
|   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); |   await page.getByRole('button', { name: 'Print', exact: true }).isEnabled(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user