From 623d783dbeaeb9da0d6e5ba741d1e283ff7fee81 Mon Sep 17 00:00:00 2001 From: SPM Date: Sat, 27 Dec 2025 16:14:53 +0100 Subject: [PATCH 1/3] Added keep open boolean field to Stock Location modal form --- src/frontend/src/components/forms/ApiForm.tsx | 11 +++++++++-- src/frontend/src/forms/CommonForms.tsx | 10 +++++++++- src/frontend/src/forms/StockForms.tsx | 6 ++++-- src/frontend/src/hooks/UseForm.tsx | 4 +++- src/frontend/src/tables/stock/StockLocationTable.tsx | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 089ce4f36bf0..a014da63bea1 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -459,9 +459,16 @@ export function ApiForm({ props.onFormSuccess(response.data, form); } - if (props.follow && props.modelType && response.data?.pk) { + const keepOpen = form.getValues()?.keep_form_open ?? false; + + if ( + props.follow && + props.modelType && + response.data?.pk && + !keepOpen + ) { // If we want to automatically follow the returned data - if (!!navigate) { + if (!!navigate && !keepOpen) { navigate(getDetailUrl(props.modelType, response.data?.pk)); } } else if (props.table) { diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 04d1da4ed955..9b608397a9c9 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; -import type { ApiFormFieldSet } from '@lib/types/Forms'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; import type { StatusCodeInterface, @@ -209,3 +209,11 @@ export function useParameterFields({ }; }, [data, modelType, fieldType, choices, modelId]); } + +export const keepFormOpenField = (create = true): ApiFormFieldType => ({ + label: 'Keep form open', + field_type: 'boolean', + exclude: true, + hidden: !create, + description: 'Keep form open after submitting' +}); diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 70476d591e2a..2ffbb39ba259 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -60,6 +60,7 @@ import { } from '../hooks/UseGenerator'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { StatusFilterOptions } from '../tables/Filter'; +import { keepFormOpenField } from './CommonForms'; /** * Construct a set of fields for creating / editing a StockItem instance @@ -1395,7 +1396,7 @@ export function useDeleteStockItem(props: StockOperationProps) { }); } -export function stockLocationFields(): ApiFormFieldSet { +export function stockLocationFields(create = false): ApiFormFieldSet { const fields: ApiFormFieldSet = { parent: { description: t`Parent stock location`, @@ -1408,7 +1409,8 @@ export function stockLocationFields(): ApiFormFieldSet { custom_icon: { field_type: 'icon' }, - location_type: {} + location_type: {}, + keep_form_open: keepFormOpenField(create) }; return fields; diff --git a/src/frontend/src/hooks/UseForm.tsx b/src/frontend/src/hooks/UseForm.tsx index 6213633e6b0a..ded7885d25b4 100644 --- a/src/frontend/src/hooks/UseForm.tsx +++ b/src/frontend/src/hooks/UseForm.tsx @@ -38,7 +38,9 @@ export function useApiFormModal(props: ApiFormModalProps) { } ], onFormSuccess: (data, form) => { - if (props.checkClose?.(data, form) ?? true) { + const keepOpen = form.getValues()?.keep_form_open ?? false; + + if (!keepOpen && (props.checkClose?.(data, form) ?? true)) { modalClose.current(); } props.onFormSuccess?.(data, form); diff --git a/src/frontend/src/tables/stock/StockLocationTable.tsx b/src/frontend/src/tables/stock/StockLocationTable.tsx index 43c338370c3f..95abac9c83ca 100644 --- a/src/frontend/src/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/tables/stock/StockLocationTable.tsx @@ -103,7 +103,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) { const newLocation = useCreateApiFormModal({ url: ApiEndpoints.stock_location_list, title: t`Add Stock Location`, - fields: stockLocationFields(), + fields: stockLocationFields(true), focus: 'name', initialData: { parent: parentId From 23daa958587a34cc7cc795164ba77dfdf7211530 Mon Sep 17 00:00:00 2001 From: spm Date: Thu, 8 Jan 2026 18:11:22 +0100 Subject: [PATCH 2/3] Rewrite keep form open field feature to avoid calling methods in form field definitions --- src/frontend/src/components/forms/ApiForm.tsx | 3 ++- src/frontend/src/forms/CommonForms.tsx | 15 ++++++++------- src/frontend/src/forms/StockForms.tsx | 5 +++-- src/frontend/src/functions/forms.tsx | 12 ++++++++++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index a014da63bea1..09c0f1c9953e 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -131,7 +131,8 @@ export function OptionsApiForm({ for (const [k, v] of Object.entries(_props.fields)) { _props.fields[k] = constructField({ field: v, - definition: optionsQuery?.data?.[k] + definition: optionsQuery?.data?.[k], + field_name: k }); // If the user has specified initial data, use that value here diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 9b608397a9c9..8c9f64b38c82 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -210,10 +210,11 @@ export function useParameterFields({ }, [data, modelType, fieldType, choices, modelId]); } -export const keepFormOpenField = (create = true): ApiFormFieldType => ({ - label: 'Keep form open', - field_type: 'boolean', - exclude: true, - hidden: !create, - description: 'Keep form open after submitting' -}); +export const ExtraFormData: Record = { + keep_form_open: { + label: 'Keep form open', + field_type: 'boolean', + exclude: true, + description: 'Keep form open after submitting' + } +}; \ No newline at end of file diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 2ffbb39ba259..82db79951aef 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -60,7 +60,6 @@ import { } from '../hooks/UseGenerator'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { StatusFilterOptions } from '../tables/Filter'; -import { keepFormOpenField } from './CommonForms'; /** * Construct a set of fields for creating / editing a StockItem instance @@ -1410,7 +1409,9 @@ export function stockLocationFields(create = false): ApiFormFieldSet { field_type: 'icon' }, location_type: {}, - keep_form_open: keepFormOpenField(create) + keep_form_open: { + hidden: !create + } }; return fields; diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index 6b2c6fb56384..a94b465b6ccd 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -5,6 +5,7 @@ import { apiUrl } from '@lib/functions/Api'; import type { PathParams } from '@lib/types/Core'; import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { invalidResponse, permissionDenied } from './notifications'; +import {ExtraFormData} from "../forms/CommonForms"; /** * Construct an API url from the provided ApiFormProps object @@ -133,13 +134,19 @@ export function mapFields( */ export function constructField({ field, - definition + definition, + field_name }: { field: ApiFormFieldType; definition?: ApiFormFieldType; + field_name?: string; }) { + + const extra_definition = ExtraFormData[field_name ?? ''] ?? {}; + const def = { ...definition, + ...extra_definition, ...field }; @@ -149,7 +156,8 @@ export function constructField({ for (const k of Object.keys(field.children ?? {})) { def.children[k] = constructField({ field: field.children?.[k] ?? {}, - definition: definition?.children?.[k] ?? {} + definition: definition?.children?.[k] ?? {}, + field_name: k }); } break; From 787b8e06a46744507a8cb57934bf8b912d33d020 Mon Sep 17 00:00:00 2001 From: SPM Date: Sun, 8 Mar 2026 12:53:45 +0100 Subject: [PATCH 3/3] Rewrite keep form open feature as common form property --- src/frontend/lib/types/Forms.tsx | 2 ++ src/frontend/src/components/forms/ApiForm.tsx | 28 +++++++++++++++---- src/frontend/src/forms/CommonForms.tsx | 11 +------- src/frontend/src/forms/StockForms.tsx | 7 ++--- src/frontend/src/functions/forms.tsx | 12 ++------ src/frontend/src/hooks/UseForm.tsx | 10 ++++--- .../src/tables/stock/StockLocationTable.tsx | 2 +- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx index 1bc3dc62f35f..42ba36acfdd3 100644 --- a/src/frontend/lib/types/Forms.tsx +++ b/src/frontend/lib/types/Forms.tsx @@ -180,6 +180,8 @@ export interface ApiFormProps { follow?: boolean; actions?: ApiFormAction[]; timeout?: number; + keepOpenOption?: boolean; + onKeepOpenChange?: (keepOpen: boolean) => void; } /** diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 09c0f1c9953e..245e949d2870 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -7,6 +7,7 @@ import { LoadingOverlay, Paper, Stack, + Switch, Text } from '@mantine/core'; import { useId } from '@mantine/hooks'; @@ -170,6 +171,11 @@ export function ApiForm({ }>) { const api = useApi(); const queryClient = useQueryClient(); + const [keepOpen, setKeepOpen] = useState(false); + + useEffect(() => { + props.onKeepOpenChange?.(keepOpen); + }, [keepOpen]); // Accessor for the navigation function (which is used to redirect the user) let navigate: NavigateFunction | null = null; @@ -460,8 +466,6 @@ export function ApiForm({ props.onFormSuccess(response.data, form); } - const keepOpen = form.getValues()?.keep_form_open ?? false; - if ( props.follow && props.modelType && @@ -596,7 +600,6 @@ export function ApiForm({ ); } - return ( @@ -681,7 +684,19 @@ export function ApiForm({ {/* Footer with Action Buttons */} -
+ + + {props.keepOpenOption && ( + setKeepOpen(e.currentTarget.checked)} + /> + )} + {props.actions?.map((action, i) => (
+
); @@ -720,7 +735,8 @@ export function CreateApiForm({ const createProps = useMemo( () => ({ ...props, - method: 'POST' + method: 'POST', + keepOpenOption: true }), [props] ); diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 8c9f64b38c82..04d1da4ed955 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; -import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; import type { StatusCodeInterface, @@ -209,12 +209,3 @@ export function useParameterFields({ }; }, [data, modelType, fieldType, choices, modelId]); } - -export const ExtraFormData: Record = { - keep_form_open: { - label: 'Keep form open', - field_type: 'boolean', - exclude: true, - description: 'Keep form open after submitting' - } -}; \ No newline at end of file diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index bb8161864706..67bbc5132769 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -1426,7 +1426,7 @@ export function useDeleteStockItem(props: StockOperationProps) { }); } -export function stockLocationFields(create = false): ApiFormFieldSet { +export function stockLocationFields(): ApiFormFieldSet { const fields: ApiFormFieldSet = { parent: { description: t`Parent stock location`, @@ -1439,10 +1439,7 @@ export function stockLocationFields(create = false): ApiFormFieldSet { custom_icon: { field_type: 'icon' }, - location_type: {}, - keep_form_open: { - hidden: !create - } + location_type: {} }; return fields; diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index a94b465b6ccd..6b2c6fb56384 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -5,7 +5,6 @@ import { apiUrl } from '@lib/functions/Api'; import type { PathParams } from '@lib/types/Core'; import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { invalidResponse, permissionDenied } from './notifications'; -import {ExtraFormData} from "../forms/CommonForms"; /** * Construct an API url from the provided ApiFormProps object @@ -134,19 +133,13 @@ export function mapFields( */ export function constructField({ field, - definition, - field_name + definition }: { field: ApiFormFieldType; definition?: ApiFormFieldType; - field_name?: string; }) { - - const extra_definition = ExtraFormData[field_name ?? ''] ?? {}; - const def = { ...definition, - ...extra_definition, ...field }; @@ -156,8 +149,7 @@ export function constructField({ for (const k of Object.keys(field.children ?? {})) { def.children[k] = constructField({ field: field.children?.[k] ?? {}, - definition: definition?.children?.[k] ?? {}, - field_name: k + definition: definition?.children?.[k] ?? {} }); } break; diff --git a/src/frontend/src/hooks/UseForm.tsx b/src/frontend/src/hooks/UseForm.tsx index ded7885d25b4..026fbbeed755 100644 --- a/src/frontend/src/hooks/UseForm.tsx +++ b/src/frontend/src/hooks/UseForm.tsx @@ -24,9 +24,12 @@ export function useApiFormModal(props: ApiFormModalProps) { return props.modalId ?? id; }, [props.modalId, id]); + const [keepOpen, setKeepOpen] = useState(false); + const formProps = useMemo( () => ({ ...props, + onKeepOpenChange: setKeepOpen, actions: [ ...(props.actions || []), { @@ -38,8 +41,6 @@ export function useApiFormModal(props: ApiFormModalProps) { } ], onFormSuccess: (data, form) => { - const keepOpen = form.getValues()?.keep_form_open ?? false; - if (!keepOpen && (props.checkClose?.(data, form) ?? true)) { modalClose.current(); } @@ -49,7 +50,7 @@ export function useApiFormModal(props: ApiFormModalProps) { props.onFormError?.(error, form); } }), - [props] + [props, keepOpen] ); const [isOpen, setIsOpen] = useState(false); @@ -96,7 +97,8 @@ export function useCreateApiFormModal(props: ApiFormModalProps) { props.successMessage === null ? null : (props.successMessage ?? t`Item Created`), - method: props.method ?? 'POST' + method: props.method ?? 'POST', + keepOpenOption: true }), [props] ); diff --git a/src/frontend/src/tables/stock/StockLocationTable.tsx b/src/frontend/src/tables/stock/StockLocationTable.tsx index 95abac9c83ca..43c338370c3f 100644 --- a/src/frontend/src/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/tables/stock/StockLocationTable.tsx @@ -103,7 +103,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) { const newLocation = useCreateApiFormModal({ url: ApiEndpoints.stock_location_list, title: t`Add Stock Location`, - fields: stockLocationFields(true), + fields: stockLocationFields(), focus: 'name', initialData: { parent: parentId