diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index adffd6063e..85e128a1fd 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -68,7 +68,7 @@ export function ApiFormField({ : definition.value ); } - }, [definition.value]); + }, [definition.value, definition.field_type]); const fieldDefinition: ApiFormFieldType = useMemo(() => { return { diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 6365d10cef..fc3ad49135 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { 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, @@ -117,32 +117,44 @@ export function useParameterTemplateFields(): ApiFormFieldSet { }, []); } -export function useParameterFields({ - modelType, - modelId -}: { - modelType: ModelType; - modelId: number; -}): ApiFormFieldSet { +/** + * Shared hook for the dynamic "value" field on parameter forms. + * + * When the user selects a parameter template, the field type for the + * corresponding value input (data / default_value) must change to match the + * template's data type (boolean, choice, related-field selection list, or + * plain string). This hook encapsulates that state so it can be reused + * across the "Add Parameter" and "Add Category Parameter" forms. + * + * @param resetDep - When this value changes all internal state is reset to + * defaults. Pass a stringified key derived from the form's context (e.g. + * `${modelType}-${modelId}`) so the field resets when the context switches. + */ +export function useDynamicParameterValueField(resetDep?: any): { + onTemplateValueChange: (value: any, record: any) => void; + valueFieldConfig: ApiFormFieldType; + reset: () => void; +} { const api = useApi(); - const user = useUserState.getState(); - - const templateCreateFields = useParameterTemplateFields(); - const [selectionListId, setSelectionListId] = useState(null); - - // Valid field choices const [choices, setChoices] = useState([]); - - // Field type for "data" input const [fieldType, setFieldType] = useState< 'string' | 'boolean' | 'choice' | 'related field' >('string'); - - // Memoized value for the "data" field const [data, setData] = useState(''); + const reset = useCallback(() => { + setSelectionListId(null); + setFieldType('string'); + setChoices([]); + setData(''); + }, []); + + useEffect(() => { + reset(); + }, [resetDep, reset]); + const fetchSelectionEntry = useCallback( (value: any) => { if (!value || !selectionListId) { @@ -151,9 +163,7 @@ export function useParameterFields({ return api .get(apiUrl(ApiEndpoints.selectionentry_list, selectionListId), { - params: { - value: value - } + params: { value: value } }) .then((response) => { if (response.data && response.data.length == 1) { @@ -166,13 +176,102 @@ export function useParameterFields({ [selectionListId] ); - // Reset the field type and choices when the model changes - useEffect(() => { - setSelectionListId(null); - setFieldType('string'); - setChoices([]); - setData(''); - }, [modelType, modelId]); + const onTemplateValueChange = useCallback( + (value: any, record: any) => { + setSelectionListId(record?.selectionlist || null); + setData(''); + + if (record?.checkbox) { + setChoices([]); + setFieldType('boolean'); + setData('false'); + } else if (record?.choices) { + const _choices: string[] = record.choices.split(','); + + if (_choices.length > 0) { + setChoices( + _choices.map((choice) => ({ + display_name: choice.trim(), + value: choice.trim() + })) + ); + setFieldType('choice'); + } else { + setChoices([]); + setFieldType('string'); + setData(''); + } + } else if (record?.selectionlist) { + setFieldType('related field'); + setData(''); + } else { + setFieldType('string'); + setData(''); + } + }, + [setFieldType, setData, setChoices] + ); + + const valueFieldConfig: ApiFormFieldType = useMemo( + () => ({ + value: data, + onValueChange: (value: any, record: any) => { + if (fieldType === 'related field' && selectionListId) { + // For related fields, store the primary key value (not the string representation) + setData(record?.value ?? value); + } else { + setData(value); + } + }, + field_type: fieldType, + choices: fieldType === 'choice' ? choices : undefined, + default: fieldType === 'boolean' ? false : undefined, + pk_field: + fieldType === 'related field' && selectionListId ? 'value' : undefined, + model: + fieldType === 'related field' && selectionListId + ? ModelType.selectionentry + : undefined, + api_url: + fieldType === 'related field' && selectionListId + ? apiUrl(ApiEndpoints.selectionentry_list, selectionListId) + : undefined, + filters: fieldType === 'related field' ? { active: true } : undefined, + adjustValue: (value: any) => { + let v: string = value.toString().trim(); + + if (fieldType === 'boolean') { + if (v.toLowerCase() !== 'true') { + v = 'false'; + } + } + + return v; + }, + singleFetchFunction: fetchSelectionEntry + }), + [data, fieldType, choices, selectionListId, fetchSelectionEntry] + ); + + return { onTemplateValueChange, valueFieldConfig, reset }; +} + +export function useParameterFields({ + modelType, + modelId +}: { + modelType: ModelType; + modelId: number; +}): ApiFormFieldSet { + const user = useUserState.getState(); + const templateCreateFields = useParameterTemplateFields(); + + const resetKey = useMemo( + () => `${modelType}-${modelId}`, + [modelType, modelId] + ); + const { onTemplateValueChange, valueFieldConfig } = + useDynamicParameterValueField(resetKey); return useMemo(() => { return { @@ -189,97 +288,17 @@ export function useParameterFields({ for_model: modelType, enabled: true }, - onValueChange: (value: any, record: any) => { - setSelectionListId(record?.selectionlist || null); - - // Adjust the type of the "data" field based on the selected template - if (record?.checkbox) { - // This is a "checkbox" field - setChoices([]); - setFieldType('boolean'); - setData('false'); - } else if (record?.choices) { - const _choices: string[] = record.choices.split(','); - - if (_choices.length > 0) { - setChoices( - _choices.map((choice) => { - return { - display_name: choice.trim(), - value: choice.trim() - }; - }) - ); - setFieldType('choice'); - } else { - setChoices([]); - setFieldType('string'); - } - } else if (record?.selectionlist) { - setFieldType('related field'); - } else { - // Default to a simple string field - setFieldType('string'); - } - }, + onValueChange: onTemplateValueChange, addCreateFields: user.isStaff() ? templateCreateFields : undefined }, - data: { - value: data, - onValueChange: (value: any, record: any) => { - if (fieldType === 'related field' && selectionListId) { - // For related fields, we need to store the selected primary key value (not the string representation) - setData(record?.value ?? value); - } else { - setData(value); - } - }, - type: fieldType, - field_type: fieldType, - choices: fieldType === 'choice' ? choices : undefined, - default: fieldType === 'boolean' ? false : undefined, - pk_field: - fieldType === 'related field' && selectionListId - ? 'value' - : undefined, - model: - fieldType === 'related field' && selectionListId - ? ModelType.selectionentry - : undefined, - api_url: - fieldType === 'related field' && selectionListId - ? apiUrl(ApiEndpoints.selectionentry_list, selectionListId) - : undefined, - filters: - fieldType === 'related field' - ? { - active: true - } - : undefined, - adjustValue: (value: any) => { - // Coerce boolean value into a string (required by backend) - - let v: string = value.toString().trim(); - - if (fieldType === 'boolean') { - if (v.toLowerCase() !== 'true') { - v = 'false'; - } - } - - return v; - }, - singleFetchFunction: fetchSelectionEntry - }, + data: valueFieldConfig, note: {} }; }, [ - data, modelType, - fieldType, - choices, modelId, - selectionListId, + onTemplateValueChange, + valueFieldConfig, templateCreateFields, user ]); diff --git a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx index 61fc0e33b9..90feb2d687 100644 --- a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx @@ -16,6 +16,7 @@ import type { TableFilter } from '@lib/types/Filters'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { TableColumn } from '@lib/types/Tables'; import { IconInfoCircle } from '@tabler/icons-react'; +import { useDynamicParameterValueField } from '../../forms/CommonForms'; import { useCreateApiFormModal, useDeleteApiFormModal, @@ -32,16 +33,21 @@ export default function PartCategoryTemplateTable({ const table = useTable('part-category-parameter-templates'); const user = useUserState(); + const { onTemplateValueChange, valueFieldConfig, reset } = + useDynamicParameterValueField(categoryId); + const formFields: ApiFormFieldSet = useMemo(() => { return { category: { value: categoryId, disabled: categoryId !== undefined }, - template: {}, - default_value: {} + template: { + onValueChange: onTemplateValueChange + }, + default_value: valueFieldConfig }; - }, [categoryId]); + }, [categoryId, onTemplateValueChange, valueFieldConfig]); const [selectedTemplate, setSelectedTemplate] = useState(0); @@ -49,6 +55,7 @@ export default function PartCategoryTemplateTable({ url: ApiEndpoints.category_parameter_list, title: t`Add Category Parameter`, fields: useMemo(() => ({ ...formFields }), [formFields]), + onOpen: reset, table: table }); @@ -57,6 +64,7 @@ export default function PartCategoryTemplateTable({ pk: selectedTemplate, title: t`Edit Category Parameter`, fields: useMemo(() => ({ ...formFields }), [formFields]), + onOpen: reset, table: table });