From 5d40fab5c4e5f3eb0b281826a3cec25a684e7a3d Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 8 Apr 2026 10:00:01 +0100 Subject: [PATCH] Extract Radio, Numeric and Decimal wrappers --- components/elements/fields/decimalField.js | 106 +++++++ components/elements/fields/numericField.js | 114 ++++++++ components/elements/fields/radioField.js | 107 +++++++ components/elements/index.js | 320 +-------------------- memory-bank/change-log.md | 28 ++ 5 files changed, 361 insertions(+), 314 deletions(-) create mode 100644 components/elements/fields/decimalField.js create mode 100644 components/elements/fields/numericField.js create mode 100644 components/elements/fields/radioField.js diff --git a/components/elements/fields/decimalField.js b/components/elements/fields/decimalField.js new file mode 100644 index 00000000..772e9a5a --- /dev/null +++ b/components/elements/fields/decimalField.js @@ -0,0 +1,106 @@ +import React from "react"; +import _ from "lodash"; +import useTranslation from "next-translate/useTranslation"; +import { Field } from "redux-form"; +import { RenderTextfield } from "./renderTextfield"; +import { RenderDecimalField } from "./renderDecimalField"; +import { validateField } from "../validationUtils"; + +const isVisibleByInclusion = ({ + parentField, + form, + formProps, + parentFieldShowOnValue +}) => + parentField != false && + !_.isEmpty(formProps[form]) && + _.has(formProps[form].values, parentField) && + parentFieldShowOnValue.indexOf( + formProps[form].values[parentField].toString() + ) > -1; + +export function DecimalField(props) { + const { + name, + validation, + form, + formProps, + parentFieldShowOnValue, + parentField + } = props; + + let { t } = useTranslation(); + + const showIfHasParentShowValue = isVisibleByInclusion({ + parentField, + form, + formProps, + parentFieldShowOnValue + }); + + const validateDecimal = (value) => { + if (!value) return t("newappeal:is-required-label"); + + const regex = /^\d{1,6}(\.\d{1,2})?$/; + + if (!regex.test(value)) { + return t("newappeal:invalid-decimal-label"); + } + + return undefined; + }; + + const requiredMessage = t("newappeal:is-required-label"); + const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label"); + const invalidPostcodeMessage = t("newappeal:invalid-postcode-label"); + + return ( + <> + {parentField != false ? ( + showIfHasParentShowValue && ( +
+ + validateField( + value, + validation, + requiredMessage, + emojiNotAllowedMessage, + invalidPostcodeMessage + ) + } + component={RenderTextfield} + label={props.label} + maxFieldLength={props.maxFieldLength} + /> +
+ ) + ) : ( +
+ +
+ )} + + ); +} diff --git a/components/elements/fields/numericField.js b/components/elements/fields/numericField.js new file mode 100644 index 00000000..6bd17454 --- /dev/null +++ b/components/elements/fields/numericField.js @@ -0,0 +1,114 @@ +import React from "react"; +import _ from "lodash"; +import useTranslation from "next-translate/useTranslation"; +import { Field } from "redux-form"; +import { RenderTextfield } from "./renderTextfield"; +import { validateField } from "../validationUtils"; + +const isVisibleByInclusion = ({ + parentField, + form, + formProps, + parentFieldShowOnValue +}) => + parentField != false && + !_.isEmpty(formProps[form]) && + _.has(formProps[form].values, parentField) && + parentFieldShowOnValue.indexOf( + formProps[form].values[parentField].toString() + ) > -1; + +export function NumericField(props) { + const { + name, + label, + validation, + form, + formProps, + parentFieldShowOnValue, + parentField + } = props; + + let { t } = useTranslation(); + + const required = (value) => { + return value || value == 0 + ? undefined + : t("newappeal:is-required-label"); + }; + const isNumber = (value) => { + const regex = /^\d+$/; + return regex.test(value) + ? undefined + : t("newappeal:invalid-number-label"); + }; + + const maxLength = (max) => (value) => + value && value.length > max + ? `Must be ${max} characters or less` + : undefined; + + const showIfHasParentShowValue = isVisibleByInclusion({ + parentField, + form, + formProps, + parentFieldShowOnValue + }); + + const requiredMessage = t("newappeal:is-required-label"); + const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label"); + const invalidPostcodeMessage = t("newappeal:invalid-postcode-label"); + + return ( + <> + {parentField != false ? ( + showIfHasParentShowValue && ( +
+ + validateField( + value, + validation, + requiredMessage, + emojiNotAllowedMessage, + invalidPostcodeMessage + ) + } + component={RenderTextfield} + label={label} + maxFieldLength={props.maxFieldLength} + /> +
+ ) + ) : ( +
+ +
+ )} + + ); +} diff --git a/components/elements/fields/radioField.js b/components/elements/fields/radioField.js new file mode 100644 index 00000000..80eae158 --- /dev/null +++ b/components/elements/fields/radioField.js @@ -0,0 +1,107 @@ +import React from "react"; +import _ from "lodash"; +import useTranslation from "next-translate/useTranslation"; +import { Field } from "redux-form"; +import { RenderRadio } from "./renderRadio"; +import { validateField } from "../validationUtils"; + +const isVisibleByInclusion = ({ + parentField, + form, + formProps, + parentFieldShowOnValue +}) => + parentField != false && + !_.isEmpty(formProps[form]) && + _.has(formProps[form].values, parentField) && + parentFieldShowOnValue.indexOf( + formProps[form].values[parentField].toString() + ) > -1; + +export function Radiofield(props) { + const { + name, + label, + datafieldname, + form, + formProps, + parentFieldShowOnValue, + parentField, + validation + } = props; + + let { t } = useTranslation(); + + const fieldOptions = props.options + .slice(1, props.options.length - 1) + .split(","); + + const showIfHasParentShowValue = isVisibleByInclusion({ + parentField, + form, + formProps, + parentFieldShowOnValue + }); + + const requiredMessage = t("newappeal:is-required-label"); + const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label"); + const invalidPostcodeMessage = t("newappeal:invalid-postcode-label"); + + return ( + <> + {parentField != false ? ( + showIfHasParentShowValue && ( + + validateField( + value, + validation, + requiredMessage, + emojiNotAllowedMessage, + invalidPostcodeMessage + ) + } + requiredDocumentLabel={props.requiredDocumentLabel} + requiredDocumentValue={props.requiredDocumentValue} + setDocumentsList={props.setDocumentsList} + documentList={props.documentList} + /> + ) + ) : ( + + validateField( + value, + validation, + requiredMessage, + emojiNotAllowedMessage, + invalidPostcodeMessage + ) + } + requiredDocumentLabel={props.requiredDocumentLabel} + requiredDocumentValue={props.requiredDocumentValue} + setDocumentsList={props.setDocumentsList} + documentList={props.documentList} + /> + )} + + ); +} diff --git a/components/elements/index.js b/components/elements/index.js index da43b0a3..550f8d17 100644 --- a/components/elements/index.js +++ b/components/elements/index.js @@ -13,12 +13,13 @@ import { getFieldTranslation } from "./helpers/translationHelpers"; import { RenderRichMultiline } from "./fields/renderRichMultiline"; import { RenderTextfield } from "./fields/renderTextfield"; import { RenderMultiline } from "./fields/renderMultiline"; -import { RenderRadio } from "./fields/renderRadio"; import { PickList } from "./fields/pickListField"; import { CheckBoxfield } from "./fields/checkBoxField"; import { DateFieldPicker } from "./fields/dateFieldPicker"; import { YesNofield } from "./fields/yesNoField"; -import { RenderDecimalField } from "./fields/renderDecimalField"; +import { Radiofield } from "./fields/radioField"; +import { NumericField } from "./fields/numericField"; +import { DecimalField } from "./fields/decimalField"; import { RenderFileUpload } from "./fields/renderFileUpload"; export { RenderSubFields } from "./fields/renderSubFields"; export { FieldArrayForm } from "./fields/fieldArrayForm"; @@ -26,6 +27,9 @@ export { PickList }; export { CheckBoxfield }; export { DateFieldPicker }; export { YesNofield }; +export { Radiofield }; +export { NumericField }; +export { DecimalField }; const getValidationMessages = (t) => ({ requiredMessage: t("newappeal:is-required-label"), @@ -489,318 +493,6 @@ export function DateField(props) { ); } -export function Radiofield(props) { - const { - name, - label, - datafieldname, - form, - formProps, - parentFieldShowOnValue, - parentField, - validation - } = props; - - const router = useRouter(); - // console.log(props.options); - let { t } = useTranslation(); - - const fieldOptions = props.options - .slice(1, props.options.length - 1) - .split(","); - - //parentFieldShowOnValue - var showIfHasParentShowValue = isVisibleByInclusion({ - parentField, - form, - formProps, - parentFieldShowOnValue - }); - - (showIfHasParentShowValue == parentField) != false && - showIfHasParentShowValue; - - const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } = - getValidationMessages(t); - return ( - <> - {parentField != false ? ( - showIfHasParentShowValue && ( - - validateField( - value, - validation, - requiredMessage, - emojiNotAllowedMessage, - invalidPostcodeMessage - ) - } // Use the external validate function - requiredDocumentLabel={props.requiredDocumentLabel} - requiredDocumentValue={props.requiredDocumentValue} - setDocumentsList={props.setDocumentsList} - documentList={props.documentList} - /> - ) - ) : ( - - validateField( - value, - validation, - requiredMessage, - emojiNotAllowedMessage, - invalidPostcodeMessage - ) - } // Use the external validate function - requiredDocumentLabel={props.requiredDocumentLabel} - requiredDocumentValue={props.requiredDocumentValue} - setDocumentsList={props.setDocumentsList} - documentList={props.documentList} - /> - )} - - ); -} - -export function NumericField(props) { - const { - name, - label, - validation, - form, - formProps, - parentFieldShowOnValue, - parentField, - maxFieldLength - } = props; - - let { t } = useTranslation(); - - const required = (value) => { - return value || value == 0 - ? undefined - : t("newappeal:is-required-label"); - }; - const isNumber = (value) => { - const regex = /^\d+$/; - return regex.test(value) - ? undefined - : t("newappeal:invalid-number-label"); - }; - - const maxLength = (max) => (value) => - value && value.length > max - ? `Must be ${max} characters or less` - : undefined; - - //parentFieldShowOnValue - var showIfHasParentShowValue = isVisibleByInclusion({ - parentField, - form, - formProps, - parentFieldShowOnValue - }); - - (showIfHasParentShowValue == parentField) != false && - showIfHasParentShowValue; - - //console.log("parentField:", parentField, showIfHasParentShowValue); - const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } = - getValidationMessages(t); - return ( - <> - {parentField != false ? ( - showIfHasParentShowValue && ( -
- - validateField( - value, - validation, - requiredMessage, - emojiNotAllowedMessage, - invalidPostcodeMessage - ) - } // Use the external validate function - component={RenderTextfield} - label={label} - maxFieldLength={props.maxFieldLength} - /> -
- ) - ) : ( -
- parseInt(value)} - // maxFieldLength={props.maxFieldLength} - - name={props.name} - id={props.name} - type="text" - className="govuk-input govuk-input--width-10" - aria-describedby={name} - //pattern="^\d*\.?\d+$" - pattern="[0-9]*" - validate={[ - required, - isNumber, - maxLength(props.maxFieldLength) - ]} - component={RenderTextfield} - label={props.label} - maxFieldLength={props.maxFieldLength} - /> -
- )} - - ); -} - -export function DecimalField(props) { - const { - name, - label, - validation, - form, - formProps, - parentFieldShowOnValue, - parentField, - maxFieldLength - } = props; - - let { t } = useTranslation(); - - //parentFieldShowOnValue - var showIfHasParentShowValue = isVisibleByInclusion({ - parentField, - form, - formProps, - parentFieldShowOnValue - }); - - (showIfHasParentShowValue == parentField) != false && - showIfHasParentShowValue; - - //console.log("parentField:", parentField, showIfHasParentShowValue); - - const validateDecimal = (value) => { - if (!value) return t("newappeal:is-required-label"); - - // Regex to match whole numbers or decimal numbers (up to 7 characters including decimal) - const regex = /^\d{1,6}(\.\d{1,2})?$/; // Up to 6 digits before the decimal, 2 after - - // Check if the value matches the regex pattern - if (!regex.test(value)) { - return t("newappeal:invalid-decimal-label"); - } - - return undefined; - }; - - const { requiredMessage, emojiNotAllowedMessage, invalidPostcodeMessage } = - getValidationMessages(t); - return ( - <> - {parentField != false ? ( - showIfHasParentShowValue && ( -
- - validateField( - value, - validation, - requiredMessage, - emojiNotAllowedMessage, - invalidPostcodeMessage - ) - } // Use the external validate function - component={RenderTextfield} - label={props.label} - maxFieldLength={props.maxFieldLength} - /> -
- ) - ) : ( -
- -
- )} - - ); -} - export function ReadOnlyfield(props) { const { name, label, value } = props; //console.log(props.value.refno); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 7449ea75..28f12758 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3670,3 +3670,31 @@ Validation: Follow-ups: - Remaining wrappers can continue as bounded slices (`Radiofield`, `NumericField`, `DecimalField`) if required. + +--- + +### CL-104: 22500 wrapper extraction bundle (`Radiofield`, `NumericField`, `DecimalField`) + +date: 2026-04-08 +author: Cline +scope: `components/elements/index.js`, `components/elements/fields/{radioField,numericField,decimalField}.js` +type: change +rationale: Continue Phase 2 with the next bounded wrapper bundle by extracting wrappers 4/5/6 from `components/elements/index.js` into dedicated field modules without behavior change. +impact: No intended behavior change; preserves EN/CY behavior, validation wiring, and accessibility semantics while reducing monolith size. +status: completed + +Summary: + +- Added `components/elements/fields/radioField.js` for `Radiofield`. +- Added `components/elements/fields/numericField.js` for `NumericField`. +- Added `components/elements/fields/decimalField.js` for `DecimalField`. +- Updated `components/elements/index.js` to import/export these wrappers from field modules. +- Removed inline `Radiofield`, `NumericField`, and `DecimalField` implementations from `index.js`. + +Validation: + +- `npx eslint components/elements/index.js components/elements/fields/radioField.js components/elements/fields/numericField.js components/elements/fields/decimalField.js` -> pass + +Follow-ups: + +- Next bounded wrappers (if needed): `ReadOnlyfield`/remaining small wrappers or additional dead-code cleanup slices.