diff --git a/designer/client/ComponentTypeEdit.tsx b/designer/client/ComponentTypeEdit.tsx index a06c2168c1..74c8c2e83a 100644 --- a/designer/client/ComponentTypeEdit.tsx +++ b/designer/client/ComponentTypeEdit.tsx @@ -6,6 +6,7 @@ import ListFieldEdit from "./components/FieldEditors/list-field-edit"; import SelectFieldEdit from "./components/FieldEditors/select-field-edit"; import { TextFieldEdit } from "./components/FieldEditors/text-field-edit"; import { MultilineTextFieldEdit } from "./multiline-text-field-edit"; +import { FreeTextFieldEdit } from "./free-text-field-edit"; import { FileUploadFieldEdit } from "./file-upload-field-edit"; import { NumberFieldEdit } from "./components/FieldEditors/number-field-edit"; import { DateFieldEdit } from "./components/FieldEditors/date-field-edit"; @@ -17,6 +18,7 @@ const componentTypeEditors = { EmailAddressField: TextFieldEdit, TelephoneNumberField: TextFieldEdit, MultilineTextField: MultilineTextFieldEdit, + FreeTextField: FreeTextFieldEdit, NumberField: NumberFieldEdit, AutocompleteField: ListFieldEdit, SelectField: SelectFieldEdit, diff --git a/designer/client/component.js b/designer/client/component.js index 082189b786..94913bd96a 100644 --- a/designer/client/component.js +++ b/designer/client/component.js @@ -18,6 +18,7 @@ export const componentTypes = { DateTimePartsField, MonthYearField, MultilineTextField, + FreeTextField, RadiosField, CheckboxesField, AutocompleteField: SelectField, @@ -86,6 +87,14 @@ function MultilineTextField() { ); } +function FreeTextField() { + return ( + + + + ); +} + function NumberField() { return ( diff --git a/designer/client/components/ComponentCreate/__tests__/ComponentCreateList.test.tsx b/designer/client/components/ComponentCreate/__tests__/ComponentCreateList.test.tsx index a820e107ec..38ee25e15f 100644 --- a/designer/client/components/ComponentCreate/__tests__/ComponentCreateList.test.tsx +++ b/designer/client/components/ComponentCreate/__tests__/ComponentCreateList.test.tsx @@ -92,6 +92,7 @@ suite("ComponentCreateList", () => { "Date time parts", "Email address", "File upload", + "Free text", "Month year parts", "Multiline text", "Number", diff --git a/designer/client/components/ComponentCreate/__tests__/__snapshots__/ComponentCreateList.jest.tsx.snap b/designer/client/components/ComponentCreate/__tests__/__snapshots__/ComponentCreateList.jest.tsx.snap index a19b2807fd..b7de5b2f18 100644 --- a/designer/client/components/ComponentCreate/__tests__/__snapshots__/ComponentCreateList.jest.tsx.snap +++ b/designer/client/components/ComponentCreate/__tests__/__snapshots__/ComponentCreateList.jest.tsx.snap @@ -220,6 +220,19 @@ exports[`ComponentCreateList should match snapshot 1`] = ` Allows users to upload files from the device they are using + + + Free text + + + A text box with basic formatting options (bold text, bullet points) for users to enter multiple lines of text into, for example as 'Additional info' + + + + {i18n("textFieldEditComponent.maxWordField.title")} + + + {i18n("textFieldEditComponent.maxWordField.helpText")} + + + dispatch({ + type: Actions.EDIT_OPTIONS_MAX_WORDS, + payload: e.target.value, + }) + } + /> + + ); +} diff --git a/designer/client/i18n/translations/en.translation.json b/designer/client/i18n/translations/en.translation.json index b5eac278e1..d54dc48c57 100644 --- a/designer/client/i18n/translations/en.translation.json +++ b/designer/client/i18n/translations/en.translation.json @@ -179,6 +179,8 @@ "MonthYearField_info": "Allows users to manually enter a Month and Year", "MultilineTextField": "Multiline text", "MultilineTextField_info": "A text box for users to enter multiple lines of text into, for example as 'Additional info'", + "FreeTextField": "Free text", + "FreeTextField_info": "A text box with basic formatting options (bold text, bullet points) for users to enter multiple lines of text into, for example as 'Additional info'", "NumberField": "Number", "NumberField_info": "A text box that users can only enter numbers into", "Para": "Paragraph", diff --git a/designer/client/styles/index.scss b/designer/client/styles/index.scss index 5449fce87a..1350100285 100644 --- a/designer/client/styles/index.scss +++ b/designer/client/styles/index.scss @@ -157,6 +157,10 @@ button.govuk-link { height: 20px; border: solid 2px #000; + &.thick-top-border { + border-top: 8px solid lightsteelblue; + } + &.tall { height: 50px; } diff --git a/designer/test/inline-condition-operators.test.tsx b/designer/test/inline-condition-operators.test.tsx index 542431333c..61cf9eedf8 100644 --- a/designer/test/inline-condition-operators.test.tsx +++ b/designer/test/inline-condition-operators.test.tsx @@ -194,6 +194,22 @@ suite("Inline condition operators", () => { }, ], }, + FreeTextField: { + cases: [ + { + operators: { + "has length": (field, value) => + `length(${field}) == ${value.value}`, + is: (field, value) => `${field} == '${value.value}'`, + "is longer than": (field, value) => + `length(${field}) > ${value.value}`, + "is not": (field, value) => `${field} != '${value.value}'`, + "is shorter than": (field, value) => + `length(${field}) < ${value.value}`, + }, + }, + ], + }, EmailAddressField: { cases: [ { diff --git a/model/src/components/component-types.ts b/model/src/components/component-types.ts index d60511a5bc..71c37cf7d4 100644 --- a/model/src/components/component-types.ts +++ b/model/src/components/component-types.ts @@ -19,6 +19,15 @@ export const ComponentTypes: ComponentDef[] = [ options: {}, schema: {}, }, + { + name: "FreeTextField", + type: "FreeTextField", + title: "Free text field", + subType: "field", + hint: "", + options: {}, + schema: {}, + }, { name: "YesNoField", type: "YesNoField", diff --git a/model/src/conditions/condition-operators.ts b/model/src/conditions/condition-operators.ts index d498b005e5..8d580c677c 100644 --- a/model/src/conditions/condition-operators.ts +++ b/model/src/conditions/condition-operators.ts @@ -79,6 +79,7 @@ export const customOperators = { ), TextField: withDefaults(textBasedFieldCustomisations), MultilineTextField: withDefaults(textBasedFieldCustomisations), + FreeTextField: withDefaults(textBasedFieldCustomisations), EmailAddressField: withDefaults(textBasedFieldCustomisations), }; diff --git a/model/src/conditions/inline-condition-operators.ts b/model/src/conditions/inline-condition-operators.ts index bc5df8cb27..c060040e0e 100644 --- a/model/src/conditions/inline-condition-operators.ts +++ b/model/src/conditions/inline-condition-operators.ts @@ -74,6 +74,7 @@ export const customOperators = { ), TextField: withDefaults(textBasedFieldCustomisations), MultilineTextField: withDefaults(textBasedFieldCustomisations), + FreeTextField: withDefaults(textBasedFieldCustomisations), EmailAddressField: withDefaults(textBasedFieldCustomisations), }; diff --git a/runner/src/server/forms/test-free-text-1.json b/runner/src/server/forms/test-free-text-1.json new file mode 100644 index 0000000000..869eadd2d2 --- /dev/null +++ b/runner/src/server/forms/test-free-text-1.json @@ -0,0 +1,50 @@ +{ + "metadata": {}, + "startPage": "/first-page", + "pages": [ + { + "title": "First page", + "path": "/first-page", + "components": [ + { + "name": "CClUon", + "options": { + "maxWords": "15" + }, + "type": "FreeTextField", + "title": "My free text field", + "hint": "asdfasdfasdfasdf" + } + ], + "next": [ + { + "path": "/second-page" + } + ] + }, + { + "path": "/second-page", + "title": "Second page", + "components": [], + "next": [ + { + "path": "/summary" + } + ] + }, + { + "title": "Summary", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [] + } + ], + "lists": [], + "sections": [], + "conditions": [], + "fees": [], + "outputs": [], + "version": 2, + "skipSummary": false, + "markAsComplete": false +} diff --git a/runner/src/server/plugins/engine/components/FreeTextField.ts b/runner/src/server/plugins/engine/components/FreeTextField.ts index 698b32d3ec..4952453498 100644 --- a/runner/src/server/plugins/engine/components/FreeTextField.ts +++ b/runner/src/server/plugins/engine/components/FreeTextField.ts @@ -44,15 +44,6 @@ export class FreeTextField extends FormComponent { } this.formSchema = this.formSchema.ruleset; - if (def.schema.max) { - this.formSchema = this.formSchema.max(def.schema.max); - this.isCharacterOrWordCount = true; - } - - if (def.schema.min) { - this.formSchema = this.formSchema.min(def.schema.min); - } - if (maxWords ?? false) { this.formSchema = this.formSchema.custom((value, helpers) => { if (inputIsOverWordCount(value, maxWords)) { @@ -91,10 +82,6 @@ export class FreeTextField extends FormComponent { ) as FreeTextFieldViewModel; viewModel.isCharacterOrWordCount = this.isCharacterOrWordCount; - if (schema.max ?? false) { - viewModel.maxlength = schema.max; - } - if (options.rows ?? false) { viewModel.rows = options.rows; } diff --git a/runner/test/cases/server/plugins/engine/FreeTextField.test.ts b/runner/test/cases/server/plugins/engine/FreeTextField.test.ts new file mode 100644 index 0000000000..3f818539e3 --- /dev/null +++ b/runner/test/cases/server/plugins/engine/FreeTextField.test.ts @@ -0,0 +1,63 @@ +import * as Code from "@hapi/code"; +import * as Lab from "@hapi/lab"; +import { FreeTextField } from "src/server/plugins/engine/components"; +import { validationOptions } from "../../../../../src/server/plugins/engine/pageControllers/validationOptions"; + +const lab = Lab.script(); +exports.lab = lab; +const { expect } = Code; +const { suite, test } = lab; + +suite("Free text field", () => { + test("Should return correct view model when not configured with max words", () => { + const def = { + name: "myComponent", + title: "My component", + hint: "a hint", + options: {}, + schema: {}, + }; + const freeTextField = new FreeTextField(def, {}); + expect(freeTextField.getViewModel({})).to.contain({ + isCharacterOrWordCount: false, + }); + }); + test("Should return correct view model when configured with max words", () => { + const def = { + name: "myComponent", + title: "My component", + hint: "a hint", + options: { + maxWords: 26, + }, + schema: {}, + }; + const freeTextField = new FreeTextField(def, {}); + expect(freeTextField.getViewModel({})).to.contain({ + isCharacterOrWordCount: true, + maxWords: 26, + }); + }); + + test("Should supply custom validation message if defined", () => { + const def = { + name: "myComponent", + title: "My component", + hint: "a hint", + options: { + required: false, + customValidationMessage: "This is a custom error", + maxWords: 2, + }, + schema: {}, + }; + const freeTextField = new FreeTextField(def, {}); + const { formSchema } = freeTextField; + + expect(formSchema.validate("a").error).to.be.undefined(); + + expect( + formSchema.validate("too many words in here").error.message + ).to.equal("This is a custom error"); + }); +}); diff --git a/version b/version index 195fd3cb40..69e3039d6d 100644 --- a/version +++ b/version @@ -1 +1 @@ -VERSION=0.1.191 +VERSION=0.1.194