mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-03 10:01:07 +00:00
Added keep open boolean field to Stock Location modal form (#11074)
* Added keep open boolean field to Stock Location modal form * Rewrite keep form open field feature to avoid calling methods in form field definitions * Rewrite keep form open feature as common form property * Removed unused artefact from previous implementation * keepOpenOption removed as default option for all create forms. Instead it's enabled on selected forms. * keepOpenOption field speed improvement - using useRef instead of useState - keepOpenSwitch moved to own component * Added keep form open feature to changelog * Updated documentation: keep form open feature added to concepts/user_interface docs * Added test case for "keep form open" feature * Changed switch selector in keep form open feature test --------- Co-authored-by: spm <jan.krajdl@cecolo.com> Co-authored-by: Oliver <oliver.henry.walters@gmail.com> Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -180,6 +180,8 @@ export interface ApiFormProps {
|
||||
follow?: boolean;
|
||||
actions?: ApiFormAction[];
|
||||
timeout?: number;
|
||||
keepOpenOption?: boolean;
|
||||
onKeepOpenChange?: (keepOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
showTimeoutNotification
|
||||
} from '../../functions/notifications';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { KeepFormOpenSwitch } from './KeepFormOpenSwitch';
|
||||
import { ApiFormField } from './fields/ApiFormField';
|
||||
|
||||
export function OptionsApiForm({
|
||||
@@ -169,6 +170,12 @@ export function ApiForm({
|
||||
}>) {
|
||||
const api = useApi();
|
||||
const queryClient = useQueryClient();
|
||||
const keepOpenRef = useRef(false);
|
||||
|
||||
const onKeepOpenChange = (v: boolean) => {
|
||||
keepOpenRef.current = v;
|
||||
props.onKeepOpenChange?.(v);
|
||||
};
|
||||
|
||||
// Accessor for the navigation function (which is used to redirect the user)
|
||||
let navigate: NavigateFunction | null = null;
|
||||
@@ -459,9 +466,14 @@ export function ApiForm({
|
||||
props.onFormSuccess(response.data, form);
|
||||
}
|
||||
|
||||
if (props.follow && props.modelType && response.data?.pk) {
|
||||
if (
|
||||
props.follow &&
|
||||
props.modelType &&
|
||||
response.data?.pk &&
|
||||
!keepOpenRef.current
|
||||
) {
|
||||
// If we want to automatically follow the returned data
|
||||
if (!!navigate) {
|
||||
if (!!navigate && !keepOpenRef.current) {
|
||||
navigate(getDetailUrl(props.modelType, response.data?.pk));
|
||||
}
|
||||
} else if (props.table) {
|
||||
@@ -588,7 +600,6 @@ export function ApiForm({
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Boundary label={`ApiForm-${id}`}>
|
||||
@@ -673,7 +684,12 @@ export function ApiForm({
|
||||
|
||||
{/* Footer with Action Buttons */}
|
||||
<Divider />
|
||||
<div>
|
||||
<Group justify='space-between'>
|
||||
<Group justify='left'>
|
||||
{props.keepOpenOption && (
|
||||
<KeepFormOpenSwitch onChange={onKeepOpenChange} />
|
||||
)}
|
||||
</Group>
|
||||
<Group justify='right'>
|
||||
{props.actions?.map((action, i) => (
|
||||
<Button
|
||||
@@ -696,7 +712,7 @@ export function ApiForm({
|
||||
{props.submitText ?? t`Submit`}
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
</Boundary>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
23
src/frontend/src/components/forms/KeepFormOpenSwitch.tsx
Normal file
23
src/frontend/src/components/forms/KeepFormOpenSwitch.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Switch } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function KeepFormOpenSwitch({
|
||||
onChange
|
||||
}: { onChange?: (v: boolean) => void }) {
|
||||
const [keepOpen, setKeepOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(keepOpen);
|
||||
}, [keepOpen]);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={keepOpen}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
label='Keep form open'
|
||||
description='Keep form open after submitting'
|
||||
onChange={(e) => setKeepOpen(e.currentTarget.checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -321,7 +321,8 @@ export function useCreateStockItem() {
|
||||
url: ApiEndpoints.stock_item_list,
|
||||
fields: fields,
|
||||
modalId: 'create-stock-item',
|
||||
title: t`Add Stock Item`
|
||||
title: t`Add Stock Item`,
|
||||
keepOpenOption: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,15 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
||||
return props.modalId ?? id;
|
||||
}, [props.modalId, id]);
|
||||
|
||||
const keepOpenRef = useRef(false);
|
||||
const setKeepOpen = (v: boolean) => {
|
||||
keepOpenRef.current = v;
|
||||
};
|
||||
|
||||
const formProps = useMemo<ApiFormModalProps>(
|
||||
() => ({
|
||||
...props,
|
||||
onKeepOpenChange: setKeepOpen,
|
||||
actions: [
|
||||
...(props.actions || []),
|
||||
{
|
||||
@@ -38,7 +44,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
||||
}
|
||||
],
|
||||
onFormSuccess: (data, form) => {
|
||||
if (props.checkClose?.(data, form) ?? true) {
|
||||
if (!keepOpenRef.current && (props.checkClose?.(data, form) ?? true)) {
|
||||
modalClose.current();
|
||||
}
|
||||
props.onFormSuccess?.(data, form);
|
||||
|
||||
@@ -218,7 +218,8 @@ export function BuildOrderTable({
|
||||
parent: parentBuildId
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.build
|
||||
modelType: ModelType.build,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
|
||||
@@ -80,7 +80,8 @@ export function CompanyTable({
|
||||
fields: companyFields(),
|
||||
initialData: params,
|
||||
follow: true,
|
||||
modelType: ModelType.company
|
||||
modelType: ModelType.company,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const [selectedCompany, setSelectedCompany] = useState<number>(0);
|
||||
|
||||
@@ -109,7 +109,8 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.partcategory,
|
||||
table: table
|
||||
table: table,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(-1);
|
||||
|
||||
@@ -407,7 +407,8 @@ export function PartListTable({
|
||||
fields: newPartFields,
|
||||
initialData: initialPartData,
|
||||
follow: true,
|
||||
modelType: ModelType.part
|
||||
modelType: ModelType.part,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const [selectedPart, setSelectedPart] = useState<any>({});
|
||||
|
||||
@@ -118,7 +118,8 @@ export function ManufacturerPartTable({
|
||||
initialData: {
|
||||
manufacturer: manufacturerId,
|
||||
part: partId
|
||||
}
|
||||
},
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const editManufacturerPart = useEditApiFormModal({
|
||||
|
||||
@@ -175,7 +175,8 @@ export function PurchaseOrderTable({
|
||||
supplier: supplierId
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.purchaseorder
|
||||
modelType: ModelType.purchaseorder,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
|
||||
@@ -210,7 +210,8 @@ export function SupplierPartTable({
|
||||
onFormSuccess: (response: any) => {
|
||||
table.refreshTable();
|
||||
},
|
||||
successMessage: t`Supplier part created`
|
||||
successMessage: t`Supplier part created`,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const supplierPlugins = usePluginsWithMixin('supplier');
|
||||
|
||||
@@ -179,7 +179,8 @@ export function ReturnOrderTable({
|
||||
customer: customerId
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.returnorder
|
||||
modelType: ModelType.returnorder,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
|
||||
@@ -125,7 +125,8 @@ export function SalesOrderTable({
|
||||
customer: customerId
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.salesorder
|
||||
modelType: ModelType.salesorder,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
|
||||
@@ -417,7 +417,8 @@ export function StockItemTable({
|
||||
// Navigate to the first result
|
||||
navigate(getDetailUrl(ModelType.stockitem, response[0].pk));
|
||||
},
|
||||
successMessage: t`Stock item created`
|
||||
successMessage: t`Stock item created`,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
|
||||
|
||||
@@ -110,7 +110,8 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
},
|
||||
follow: true,
|
||||
modelType: ModelType.stocklocation,
|
||||
table: table
|
||||
table: table,
|
||||
keepOpenOption: true
|
||||
});
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<number>(-1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Unit tests for form validation, rendering, etc */
|
||||
import test from 'playwright/test';
|
||||
import { expect, test } from 'playwright/test';
|
||||
import { stevenuser } from './defaults';
|
||||
import { navigate } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
@@ -134,3 +134,37 @@ test('Forms - Supplier Validation', async ({ browser }) => {
|
||||
await page.getByText('Form Error').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
|
||||
test('Forms - Keep form open option', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
user: stevenuser,
|
||||
url: 'stock/location/index/sublocations'
|
||||
});
|
||||
await page.waitForURL('**/stock/location/index/**');
|
||||
|
||||
await page.getByLabel('action-button-add-stock-location').click();
|
||||
|
||||
// Generate unique location name
|
||||
const locationName = `New Sublocation ${new Date().getTime()}`;
|
||||
|
||||
await page.getByLabel('text-field-name', { exact: true }).fill(locationName);
|
||||
|
||||
// Check keep form open switch and submit
|
||||
await page.getByRole('switch', { name: 'Keep form open' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Location should be created, form should remain opened
|
||||
await page.getByText('Item Created').waitFor();
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
|
||||
// Create another location and uncheck this option
|
||||
await page
|
||||
.getByLabel('text-field-name', { exact: true })
|
||||
.fill(`Another ${locationName}`);
|
||||
await page.getByRole('switch', { name: 'Keep form open' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Location should be created, and the form (modal) should disappear
|
||||
await page.getByText('Item Created').waitFor();
|
||||
await expect(page.getByRole('dialog')).toBeHidden();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user