mirror of
https://github.com/inventree/InvenTree.git
synced 2025-11-30 09:20:03 +00:00
[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
This commit is contained in:
@@ -24,11 +24,7 @@ import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { isTrue } from '@lib/functions/Conversion';
|
import { isTrue } from '@lib/functions/Conversion';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import type {
|
import type { ApiFormFieldSet, ApiFormProps } from '@lib/types/Forms';
|
||||||
ApiFormFieldSet,
|
|
||||||
ApiFormFieldType,
|
|
||||||
ApiFormProps
|
|
||||||
} from '@lib/types/Forms';
|
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import {
|
import {
|
||||||
type NestedDict,
|
type NestedDict,
|
||||||
@@ -46,9 +42,11 @@ import { ApiFormField } from './fields/ApiFormField';
|
|||||||
|
|
||||||
export function OptionsApiForm({
|
export function OptionsApiForm({
|
||||||
props: _props,
|
props: _props,
|
||||||
|
opened,
|
||||||
id: pId
|
id: pId
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
props: ApiFormProps;
|
props: ApiFormProps;
|
||||||
|
opened?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
}>) {
|
}>) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
@@ -75,24 +73,26 @@ export function OptionsApiForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const optionsQuery = useQuery({
|
const optionsQuery = useQuery({
|
||||||
enabled: true,
|
enabled: opened !== false && props.ignorePermissionCheck !== true,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'form-options-data',
|
'form-options-data',
|
||||||
id,
|
id,
|
||||||
|
opened,
|
||||||
|
props.ignorePermissionCheck,
|
||||||
props.method,
|
props.method,
|
||||||
props.url,
|
props.url,
|
||||||
props.pk,
|
props.pk,
|
||||||
props.pathParams
|
props.pathParams
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await api.options(url);
|
if (props.ignorePermissionCheck === true || opened === false) {
|
||||||
let fields: Record<string, ApiFormFieldType> | null = {};
|
return {};
|
||||||
if (!props.ignorePermissionCheck) {
|
|
||||||
fields = extractAvailableFields(response, props.method);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return api.options(url).then((response: any) => {
|
||||||
|
return extractAvailableFields(response, props.method);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
throwOnError: (error: any) => {
|
throwOnError: (error: any) => {
|
||||||
if (error.response) {
|
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 formProps: ApiFormProps = useMemo(() => {
|
||||||
const _props = { ...props };
|
const _props = { ...props };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Alert, Divider, Stack } from '@mantine/core';
|
import { Alert, Divider, Stack } from '@mantine/core';
|
||||||
import { useId } from '@mantine/hooks';
|
import { useId } from '@mantine/hooks';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFormModalProps,
|
ApiFormModalProps,
|
||||||
@@ -50,14 +50,18 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
|||||||
[props]
|
[props]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const modal = useModal({
|
const modal = useModal({
|
||||||
id: modalId,
|
id: modalId,
|
||||||
title: formProps.title,
|
title: formProps.title,
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
|
setIsOpen(true);
|
||||||
modalState.setModalOpen(modalId, true);
|
modalState.setModalOpen(modalId, true);
|
||||||
formProps.onOpen?.();
|
formProps.onOpen?.();
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
|
setIsOpen(false);
|
||||||
modalState.setModalOpen(modalId, false);
|
modalState.setModalOpen(modalId, false);
|
||||||
formProps.onClose?.();
|
formProps.onClose?.();
|
||||||
},
|
},
|
||||||
@@ -66,7 +70,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
|||||||
children: (
|
children: (
|
||||||
<Stack gap={'xs'}>
|
<Stack gap={'xs'}>
|
||||||
<Divider />
|
<Divider />
|
||||||
<OptionsApiForm props={formProps} id={modalId} />
|
<OptionsApiForm props={formProps} id={modalId} opened={isOpen} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,6 +99,50 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
|||||||
.waitFor();
|
.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 }) => {
|
test('Build Order - Calendar', async ({ browser }) => {
|
||||||
const page = await doCachedLogin(browser);
|
const page = await doCachedLogin(browser);
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ test('Parts - Details', async ({ browser }) => {
|
|||||||
|
|
||||||
// Depending on the state of other tests, the "In Production" value may vary
|
// Depending on the state of other tests, the "In Production" value may vary
|
||||||
// This could be either 4 / 49, or 5 / 49
|
// This could be either 4 / 49, or 5 / 49
|
||||||
await page.getByText(/[4|5] \/ 49/).waitFor();
|
await page.getByText(/[4|5] \/ \d+/).waitFor();
|
||||||
|
|
||||||
// Badges
|
// Badges
|
||||||
await page.getByText('Required: 10').waitFor();
|
await page.getByText('Required: 10').waitFor();
|
||||||
@@ -232,14 +232,14 @@ test('Parts - Requirements', async ({ browser }) => {
|
|||||||
// Check top-level badges
|
// Check top-level badges
|
||||||
await page.getByText('In Stock: 209').waitFor();
|
await page.getByText('In Stock: 209').waitFor();
|
||||||
await page.getByText('Available: 204').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();
|
await page.getByText('In Production: 24').waitFor();
|
||||||
|
|
||||||
// Check requirements details
|
// Check requirements details
|
||||||
await page.getByText('204 / 209').waitFor(); // Available stock
|
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('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
|
// Let's check out the "variants" for this part, too
|
||||||
await navigate(page, 'part/81/details'); // WID-REV-A
|
await navigate(page, 'part/81/details'); // WID-REV-A
|
||||||
|
|||||||
Reference in New Issue
Block a user