From 686073077d69050494bf1d42cb82a10c75907182 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 13 Nov 2025 06:40:34 +0000 Subject: [PATCH 1/8] Fix for string form fields - replace null values with empty strings --- src/frontend/src/components/forms/ApiForm.tsx | 22 +++++++++++++++---- .../src/components/forms/fields/TextField.tsx | 5 +++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 632677f69163..a22d0763f837 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -375,9 +375,20 @@ export function ApiForm({ hasFiles = true; } - // Ensure any boolean values are actually boolean - if (field_type === 'boolean') { - value = isTrue(value) || false; + // Special consideration for various field types + switch (field_type) { + case 'boolean': + // Ensure boolean values are actually boolean + value = isTrue(value) || false; + break; + case 'string': + // Replace null string values with an empty string + if (value === null) { + value = ''; + } + break; + default: + break; } // Stringify any JSON objects @@ -386,7 +397,9 @@ export function ApiForm({ case 'file upload': break; default: - value = JSON.stringify(value); + if (value !== null && value !== undefined) { + value = JSON.stringify(value); + } break; } } @@ -395,6 +408,7 @@ export function ApiForm({ // Remove the field from the data delete jsonData[key]; } else if (value != undefined) { + jsonData[key] = value; formData.append(key, value); } }); diff --git a/src/frontend/src/components/forms/fields/TextField.tsx b/src/frontend/src/components/forms/fields/TextField.tsx index 8e95011b4b65..d47fe92d15a9 100644 --- a/src/frontend/src/components/forms/fields/TextField.tsx +++ b/src/frontend/src/components/forms/fields/TextField.tsx @@ -46,6 +46,11 @@ export default function TextField({ useEffect(() => { setRawText(value || ''); + + if (value === null) { + // Enforce empty string for null values + onChange(''); + } }, [value]); const onTextChange = useCallback((value: any) => { From 8970e069cef61c1ad3a3c8db8c4e27e960fd3366 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 13 Nov 2025 07:19:52 +0000 Subject: [PATCH 2/8] Expose more serializer metadata --- src/backend/InvenTree/InvenTree/metadata.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index 15733fa3de5d..c145884cea70 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -221,7 +221,7 @@ def get_serializer_info(self, serializer): # Attributes to copy extra attributes from the model to the field (if they don't exist) # Note that the attributes may be named differently on the underlying model! - extra_attributes = { + extra_model_attributes = { 'help_text': 'help_text', 'max_length': 'max_length', 'label': 'verbose_name', @@ -260,7 +260,7 @@ def get_serializer_info(self, serializer): elif name in model_default_values: serializer_info[name]['default'] = model_default_values[name] - for field_key, model_key in extra_attributes.items(): + for field_key, model_key in extra_model_attributes.items(): field_value = getattr(serializer.fields[name], field_key, None) model_value = getattr(field, model_key, None) @@ -291,7 +291,7 @@ def get_serializer_info(self, serializer): relation.model_field.get_limit_choices_to() ) - for field_key, model_key in extra_attributes.items(): + for field_key, model_key in extra_model_attributes.items(): field_value = getattr(serializer.fields[name], field_key, None) model_value = getattr(relation.model_field, model_key, None) @@ -420,6 +420,13 @@ def get_field_info(self, field): if field_info['type'] == 'dependent field': field_info['depends_on'] = field.depends_on + # Extends with extra attributes from the serializer + extra_field_attributes = ['allow_blank', 'allow_null'] + + for attr in extra_field_attributes: + if hasattr(field, attr): + field_info[attr] = getattr(field, attr) + # Extend field info if the field has a get_field_info method if ( not field_info.get('read_only') From 266f74b963d9abaf1e18dbae208c29ed9177cfc5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 13 Nov 2025 07:20:05 +0000 Subject: [PATCH 3/8] Check if null values are not allowed --- src/frontend/lib/types/Forms.tsx | 4 ++++ src/frontend/src/components/forms/ApiForm.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx index 364dc38e6fb5..f37a291921be 100644 --- a/src/frontend/lib/types/Forms.tsx +++ b/src/frontend/lib/types/Forms.tsx @@ -48,6 +48,8 @@ export type ApiFormFieldHeader = { * @param model : The model to use for related fields * @param filters : Optional API filters to apply to related fields * @param required : Whether the field is required + * @param allow_null: Whether the field allows null values + * @param allow_blank: Whether the field allows blank values * @param hidden : Whether the field is hidden * @param disabled : Whether the field is disabled * @param error : Optional error message to display @@ -103,6 +105,8 @@ export type ApiFormFieldType = { choices?: ApiFormFieldChoice[]; hidden?: boolean; disabled?: boolean; + allow_null?: boolean; + allow_blank?: boolean; exclude?: boolean; read_only?: boolean; placeholder?: string; diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index a22d0763f837..3759c056dc1d 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -368,8 +368,9 @@ export function ApiForm({ Object.keys(data).forEach((key: string) => { let value: any = data[key]; - const field_type = fields[key]?.field_type; - const exclude = fields[key]?.exclude; + const field: ApiFormFieldType = fields[key] ?? {}; + const field_type = field?.field_type; + const exclude = field?.exclude; if (field_type == 'file upload' && !!value) { hasFiles = true; @@ -383,7 +384,7 @@ export function ApiForm({ break; case 'string': // Replace null string values with an empty string - if (value === null) { + if (value === null && field?.allow_null == false) { value = ''; } break; From d4a70325058418aefd5d080b9fc0592425e1dd45 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 16 Nov 2025 11:50:18 +0000 Subject: [PATCH 4/8] Fix type --- src/frontend/src/components/forms/ApiForm.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 0a01621f31b5..81e48d088ba8 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -24,7 +24,11 @@ import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { isTrue } from '@lib/functions/Conversion'; import { getDetailUrl } from '@lib/functions/Navigation'; -import type { ApiFormFieldSet, ApiFormProps } from '@lib/types/Forms'; +import type { + ApiFormFieldSet, + ApiFormFieldType, + ApiFormProps +} from '@lib/types/Forms'; import { useApi } from '../../contexts/ApiContext'; import { type NestedDict, From 70e82357e8521e6e1fe572eceafdcd8cc205b62f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 16 Nov 2025 21:32:03 +0000 Subject: [PATCH 5/8] Try removing feature --- src/backend/InvenTree/InvenTree/metadata.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index c145884cea70..62084a0f4430 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -421,7 +421,8 @@ def get_field_info(self, field): field_info['depends_on'] = field.depends_on # Extends with extra attributes from the serializer - extra_field_attributes = ['allow_blank', 'allow_null'] + # TODO: Re-implment these, trying to work out why CI is failing + extra_field_attributes = [] # ['allow_blank', 'allow_null'] for attr in extra_field_attributes: if hasattr(field, attr): From fa2c94a7e23ee5a6abff412c719c5416370f284d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Nov 2025 08:53:33 +0000 Subject: [PATCH 6/8] Reduce deltas --- src/backend/InvenTree/InvenTree/metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index 62084a0f4430..dea046044af0 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -221,7 +221,7 @@ def get_serializer_info(self, serializer): # Attributes to copy extra attributes from the model to the field (if they don't exist) # Note that the attributes may be named differently on the underlying model! - extra_model_attributes = { + extra_attributes = { 'help_text': 'help_text', 'max_length': 'max_length', 'label': 'verbose_name', @@ -260,7 +260,7 @@ def get_serializer_info(self, serializer): elif name in model_default_values: serializer_info[name]['default'] = model_default_values[name] - for field_key, model_key in extra_model_attributes.items(): + for field_key, model_key in extra_attributes.items(): field_value = getattr(serializer.fields[name], field_key, None) model_value = getattr(field, model_key, None) @@ -291,7 +291,7 @@ def get_serializer_info(self, serializer): relation.model_field.get_limit_choices_to() ) - for field_key, model_key in extra_model_attributes.items(): + for field_key, model_key in extra_attributes.items(): field_value = getattr(serializer.fields[name], field_key, None) model_value = getattr(relation.model_field, model_key, None) From 622fc725a301f0093f851c83f07101b880a3c6ea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Nov 2025 09:52:08 +0000 Subject: [PATCH 7/8] Remove extra field attrs entirely (for testing) --- src/backend/InvenTree/InvenTree/metadata.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index dea046044af0..15733fa3de5d 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -420,14 +420,6 @@ def get_field_info(self, field): if field_info['type'] == 'dependent field': field_info['depends_on'] = field.depends_on - # Extends with extra attributes from the serializer - # TODO: Re-implment these, trying to work out why CI is failing - extra_field_attributes = [] # ['allow_blank', 'allow_null'] - - for attr in extra_field_attributes: - if hasattr(field, attr): - field_info[attr] = getattr(field, attr) - # Extend field info if the field has a get_field_info method if ( not field_info.get('read_only') From a5c139375068d7bf83b10f2c2328ac574e185088 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Nov 2025 20:47:00 +0000 Subject: [PATCH 8/8] Comment out changes --- src/backend/InvenTree/InvenTree/metadata.py | 7 +++++++ src/frontend/src/components/forms/ApiForm.tsx | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index 15733fa3de5d..e7fb922d482a 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -420,6 +420,13 @@ def get_field_info(self, field): if field_info['type'] == 'dependent field': field_info['depends_on'] = field.depends_on + # Extends with extra attributes from the serializer + extra_field_attributes = ['allow_blank', 'allow_null'] + + for attr in extra_field_attributes: + if hasattr(field, attr): + field_info[attr] = getattr(field, attr) + # Extend field info if the field has a get_field_info method if ( not field_info.get('read_only') diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 81e48d088ba8..c88e4a592e41 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -410,7 +410,7 @@ export function ApiForm({ break; default: if (value !== null && value !== undefined) { - value = JSON.stringify(value); + // value = JSON.stringify(value); } break; } @@ -420,7 +420,7 @@ export function ApiForm({ // Remove the field from the data delete jsonData[key]; } else if (value != undefined) { - jsonData[key] = value; + // jsonData[key] = value; formData.append(key, value); } });