From 770f7a292ee7a303cdf2f70fbc4d619e91f8f21d Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 16 Nov 2025 21:59:27 +1100 Subject: [PATCH] [UI] Fix for form OPTIONS query (#10840) * [UI] Fix for form OPTIONS query - Fetch OPTIONs each time form is opened - Ensure default values are filled correctly - Prevent issues with latching form state * Add comment * Add playwright test - Check that the reference field increments properly * Fix other Playwright tests --- src/frontend/src/components/forms/ApiForm.tsx | 29 +++++++----- src/frontend/src/hooks/UseForm.tsx | 8 +++- src/frontend/tests/pages/pui_build.spec.ts | 44 +++++++++++++++++++ src/frontend/tests/pages/pui_part.spec.ts | 8 ++-- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 632677f691..a9a8606363 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -24,11 +24,7 @@ import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { isTrue } from '@lib/functions/Conversion'; import { getDetailUrl } from '@lib/functions/Navigation'; -import type { - ApiFormFieldSet, - ApiFormFieldType, - ApiFormProps -} from '@lib/types/Forms'; +import type { ApiFormFieldSet, ApiFormProps } from '@lib/types/Forms'; import { useApi } from '../../contexts/ApiContext'; import { type NestedDict, @@ -46,9 +42,11 @@ import { ApiFormField } from './fields/ApiFormField'; export function OptionsApiForm({ props: _props, + opened, id: pId }: Readonly<{ props: ApiFormProps; + opened?: boolean; id?: string; }>) { const api = useApi(); @@ -75,24 +73,26 @@ export function OptionsApiForm({ ); const optionsQuery = useQuery({ - enabled: true, + enabled: opened !== false && props.ignorePermissionCheck !== true, refetchOnMount: false, queryKey: [ 'form-options-data', id, + opened, + props.ignorePermissionCheck, props.method, props.url, props.pk, props.pathParams ], queryFn: async () => { - const response = await api.options(url); - let fields: Record | null = {}; - if (!props.ignorePermissionCheck) { - fields = extractAvailableFields(response, props.method); + if (props.ignorePermissionCheck === true || opened === false) { + return {}; } - return fields; + return api.options(url).then((response: any) => { + return extractAvailableFields(response, props.method); + }); }, throwOnError: (error: any) => { if (error.response) { @@ -110,6 +110,13 @@ export function OptionsApiForm({ } }); + // Refetch form options whenever the modal is opened + useEffect(() => { + if (opened !== false) { + optionsQuery.refetch(); + } + }, [opened]); + const formProps: ApiFormProps = useMemo(() => { const _props = { ...props }; diff --git a/src/frontend/src/hooks/UseForm.tsx b/src/frontend/src/hooks/UseForm.tsx index 878d0daf82..31e732b314 100644 --- a/src/frontend/src/hooks/UseForm.tsx +++ b/src/frontend/src/hooks/UseForm.tsx @@ -1,7 +1,7 @@ import { t } from '@lingui/core/macro'; import { Alert, Divider, Stack } from '@mantine/core'; import { useId } from '@mantine/hooks'; -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { ApiFormModalProps, @@ -50,14 +50,18 @@ export function useApiFormModal(props: ApiFormModalProps) { [props] ); + const [isOpen, setIsOpen] = useState(false); + const modal = useModal({ id: modalId, title: formProps.title, onOpen: () => { + setIsOpen(true); modalState.setModalOpen(modalId, true); formProps.onOpen?.(); }, onClose: () => { + setIsOpen(false); modalState.setModalOpen(modalId, false); formProps.onClose?.(); }, @@ -66,7 +70,7 @@ export function useApiFormModal(props: ApiFormModalProps) { children: ( - + ) }); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 8817487e1f..74dd51f41e 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -99,6 +99,50 @@ test('Build Order - Basic Tests', async ({ browser }) => { .waitFor(); }); +// Test that the build order reference field increments correctly +test('Build Order - Reference', async ({ browser }) => { + const page = await doCachedLogin(browser, { + url: 'manufacturing/index/buildorders' + }); + + await page + .getByRole('button', { name: 'action-button-add-build-order' }) + .click(); + await page.getByRole('button', { name: 'Submit' }).waitFor(); + + // Grab the next BuildOrder reference + const reference: string = await page + .getByRole('textbox', { name: 'text-field-reference' }) + .inputValue(); + expect(reference).toMatch(/BO\d+/); + + // Select a part + await page.getByLabel('related-field-part').fill('MAST'); + await page.getByText('MAST | Master Assembly').click(); + + // Submit the form + await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByText('Item Created').waitFor(); + + // Back to the "build order" page - to create a new order + await navigate(page, 'manufacturing/index/buildorders'); + + await page + .getByRole('button', { name: 'action-button-add-build-order' }) + .click(); + await page.getByRole('button', { name: 'Submit' }).waitFor(); + + const nextReference: string = await page + .getByRole('textbox', { name: 'text-field-reference' }) + .inputValue(); + expect(nextReference).toMatch(/BO\d+/); + + // Ensure that the reference has incremented + const refNumber = Number(reference.replace('BO', '')); + const nextRefNumber = Number(nextReference.replace('BO', '')); + expect(nextRefNumber).toBe(refNumber + 1); +}); + test('Build Order - Calendar', async ({ browser }) => { const page = await doCachedLogin(browser); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 96cdbdf35b..97bf308d67 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -212,7 +212,7 @@ test('Parts - Details', async ({ browser }) => { // Depending on the state of other tests, the "In Production" value may vary // This could be either 4 / 49, or 5 / 49 - await page.getByText(/[4|5] \/ 49/).waitFor(); + await page.getByText(/[4|5] \/ \d+/).waitFor(); // Badges await page.getByText('Required: 10').waitFor(); @@ -232,14 +232,14 @@ test('Parts - Requirements', async ({ browser }) => { // Check top-level badges await page.getByText('In Stock: 209').waitFor(); await page.getByText('Available: 204').waitFor(); - await page.getByText('Required: 275').waitFor(); + await page.getByText(/Required: 2\d+/).waitFor(); await page.getByText('In Production: 24').waitFor(); // Check requirements details await page.getByText('204 / 209').waitFor(); // Available stock - await page.getByText('0 / 100').waitFor(); // Allocated to build orders + await page.getByText(/0 \/ 1\d+/).waitFor(); // Allocated to build orders await page.getByText('5 / 175').waitFor(); // Allocated to sales orders - await page.getByText('24 / 214').waitFor(); // In production + await page.getByText(/24 \/ 2\d+/).waitFor(); // In production // Let's check out the "variants" for this part, too await navigate(page, 'part/81/details'); // WID-REV-A