Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
623d783
Added keep open boolean field to Stock Location modal form
spamik Dec 27, 2025
23daa95
Rewrite keep form open field feature to avoid calling methods in form…
Jan 8, 2026
fdec4b5
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Feb 12, 2026
a4b4566
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Feb 12, 2026
c5cc28f
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Feb 12, 2026
c8af24c
Merge branch 'master' into form-keep-open-feature
matmair Feb 12, 2026
d8adbd8
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 3, 2026
787b8e0
Rewrite keep form open feature as common form property
spamik Mar 8, 2026
045e8c7
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 9, 2026
30cc13b
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 10, 2026
0d7ed88
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 14, 2026
3dc51a3
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 14, 2026
dde261b
Removed unused artefact from previous implementation
spamik Mar 14, 2026
ce614e6
keepOpenOption removed as default option for all create forms. Instea…
spamik Mar 14, 2026
c423bbb
keepOpenOption field speed improvement
spamik Mar 14, 2026
86ec430
Added keep form open feature to changelog
spamik Mar 15, 2026
9eede1c
Updated documentation: keep form open feature added to concepts/user_…
spamik Mar 15, 2026
182ed35
Merge branch 'master' into form-keep-open-feature
SchrodingersGat Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality.
- [#11374](https://github.com/inventree/InvenTree/pull/11374) adds `updated_at` field on purchase, sales and return orders.
- [#11074](https://github.com/inventree/InvenTree/pull/11074) adds "Keep form open" option on create form which leaves dialog with form opened after form submitting.

### Changed

Expand Down
Binary file modified docs/docs/assets/images/concepts/ui_form_add_part.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/docs/concepts/user_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ Example: Creating a new part via the "Add Part" form:

{{ image("concepts/ui_form_add_part.png", "Add Part Button") }}

On several forms is displayed option "Keep form open" in bottom part of the form on left side of Submit button (option is visible on the screenshot above). When this switch is turned on, form window is not closed after submit and filled form data is not reset. This is useful for creating more entries at one time with similar properties (e.g. only different number in name).

### Data Editing

Example: Editing an existing purchase order via the "Edit Purchase Order" form:
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/lib/types/Forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ export interface ApiFormProps {
follow?: boolean;
actions?: ApiFormAction[];
timeout?: number;
keepOpenOption?: boolean;
onKeepOpenChange?: (keepOpen: boolean) => void;
}

/**
Expand Down
28 changes: 22 additions & 6 deletions src/frontend/src/components/forms/ApiForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -588,7 +600,6 @@ export function ApiForm({
</Paper>
);
}

return (
<Stack>
<Boundary label={`ApiForm-${id}`}>
Expand Down Expand Up @@ -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
Expand All @@ -696,7 +712,7 @@ export function ApiForm({
{props.submitText ?? t`Submit`}
</Button>
</Group>
</div>
</Group>
</Boundary>
</Stack>
);
Expand Down
23 changes: 23 additions & 0 deletions src/frontend/src/components/forms/KeepFormOpenSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Switch } from '@mantine/core';
import { useEffect, useState } from 'react';

export function KeepFormOpenSwitch({
onChange
}: { onChange?: (v: boolean) => void }) {

Check warning on line 6 in src/frontend/src/components/forms/KeepFormOpenSwitch.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=inventree_InvenTree&issues=AZztRUrZOWk8A1rCAyvE&open=AZztRUrZOWk8A1rCAyvE&pullRequest=11074
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)}
/>
);
}
3 changes: 2 additions & 1 deletion src/frontend/src/forms/StockForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}

Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/hooks/UseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 || []),
{
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/build/BuildOrderTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ export function BuildOrderTable({
parent: parentBuildId
},
follow: true,
modelType: ModelType.build
modelType: ModelType.build,
keepOpenOption: true
});

const tableActions = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/company/CompanyTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/part/PartCategoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/part/PartTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export function ManufacturerPartTable({
initialData: {
manufacturer: manufacturerId,
part: partId
}
},
keepOpenOption: true
});

const editManufacturerPart = useEditApiFormModal({
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export function PurchaseOrderTable({
supplier: supplierId
},
follow: true,
modelType: ModelType.purchaseorder
modelType: ModelType.purchaseorder,
keepOpenOption: true
});

const tableActions = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/purchasing/SupplierPartTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/sales/ReturnOrderTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ export function ReturnOrderTable({
customer: customerId
},
follow: true,
modelType: ModelType.returnorder
modelType: ModelType.returnorder,
keepOpenOption: true
});

const tableActions = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/sales/SalesOrderTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export function SalesOrderTable({
customer: customerId
},
follow: true,
modelType: ModelType.salesorder
modelType: ModelType.salesorder,
keepOpenOption: true
});

const tableActions = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/stock/StockItemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,8 @@ export function StockItemTable({
// Navigate to the first result
navigate(getDetailUrl(ModelType.stockitem, response[0].pk));
},
successMessage: t`Stock item serialized`
successMessage: t`Stock item serialized`,
keepOpenOption: true
});

const [partsToOrder, setPartsToOrder] = useState<any[]>([]);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/tables/stock/StockLocationTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading