From 402acddc1010b271192e0d545e5bc510d6a4a8c5 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Sat, 9 Nov 2024 07:09:49 -0600 Subject: [PATCH 1/3] Replace "atj-platform" references with "forms", due to repo rename (#380) --- apps/spotlight/src/lib/github.ts | 4 ++-- infra/cdktf/__tests__/main-test.ts | 2 +- infra/cdktf/src/lib/cloud.gov/node-astro.ts | 2 +- packages/server/src/lib/github.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/spotlight/src/lib/github.ts b/apps/spotlight/src/lib/github.ts index b979f6d8..2d638567 100644 --- a/apps/spotlight/src/lib/github.ts +++ b/apps/spotlight/src/lib/github.ts @@ -7,7 +7,7 @@ export type GithubRepository = { export const DEFAULT_REPOSITORY: GithubRepository = { owner: 'gsa-tts', - repository: 'atj-platform', + repository: 'forms', branch: 'main', commit: 'main', }; @@ -28,7 +28,7 @@ export const getGithubRepository = async ( const { execSync } = await import('child_process'); return { owner: env.OWNER || 'gsa-tts', - repository: env.REPOSITORY || 'atj-platform', + repository: env.REPOSITORY || 'forms', branch: env.BRANCH || 'main', commit: execSync('git rev-parse HEAD').toString().trim(), }; diff --git a/infra/cdktf/__tests__/main-test.ts b/infra/cdktf/__tests__/main-test.ts index dc82a2af..22e4574e 100644 --- a/infra/cdktf/__tests__/main-test.ts +++ b/infra/cdktf/__tests__/main-test.ts @@ -1,5 +1,5 @@ import 'cdktf/lib/testing/adapters/jest'; -describe('atj-platform app stack', () => { +describe('Forms Platform app stack', () => { it.todo('should be tested'); }); diff --git a/infra/cdktf/src/lib/cloud.gov/node-astro.ts b/infra/cdktf/src/lib/cloud.gov/node-astro.ts index 3c3915d1..45e8be40 100644 --- a/infra/cdktf/src/lib/cloud.gov/node-astro.ts +++ b/infra/cdktf/src/lib/cloud.gov/node-astro.ts @@ -72,7 +72,7 @@ export class AstroService extends Construct { new cloudfoundry.app.App(this, `${id}-app`, { name: `${id}-app`, space: spaceId, - dockerImage: `ghcr.io/gsa-tts/atj-platform/${imageName}`, + dockerImage: `ghcr.io/gsa-tts/forms/${imageName}`, memory: 1024, diskQuota: 4096, healthCheckType: 'http', diff --git a/packages/server/src/lib/github.ts b/packages/server/src/lib/github.ts index b979f6d8..2d638567 100644 --- a/packages/server/src/lib/github.ts +++ b/packages/server/src/lib/github.ts @@ -7,7 +7,7 @@ export type GithubRepository = { export const DEFAULT_REPOSITORY: GithubRepository = { owner: 'gsa-tts', - repository: 'atj-platform', + repository: 'forms', branch: 'main', commit: 'main', }; @@ -28,7 +28,7 @@ export const getGithubRepository = async ( const { execSync } = await import('child_process'); return { owner: env.OWNER || 'gsa-tts', - repository: env.REPOSITORY || 'atj-platform', + repository: env.REPOSITORY || 'forms', branch: env.BRANCH || 'main', commit: execSync('git rev-parse HEAD').toString().trim(), }; From 4f3d810b07014810cc57caca2a6a972d8eb8f92c Mon Sep 17 00:00:00 2001 From: Jim Moffet Date: Sat, 9 Nov 2024 05:10:29 -0800 Subject: [PATCH 2/3] Update README with new name (#372) * update readme * don't update pnpm-lock.yaml --------- Co-authored-by: Daniel Naab --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 226a8c38..0c7a06a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 10x Access to Justice Tooling +# 10x Forms Platform -Test bed for ATJ platform tooling, completed as part of the [10x Digital Access to Justice Platform](https://trello.com/c/25Jl6NwJ/207-digital-access-to-justice-platform) project. +Test bed for 10x forms tooling, completed as part of the [10x Forms Platform](https://github.com/orgs/GSA-TTS/projects/38?pane=issue&itemId=58755590&issue=GSA-TTS%7C10x-projects%7C29) project. ## Overview @@ -17,13 +17,13 @@ The platform is made up of the following high-level terms. ### Key personas -- Content authors: legal experts who craft guided interview experiences via a "no code" interface -- Self-represented litigants (SREs): end-users who interact with the court via guided interviews created by content authors +- Form Builders: government program office staff or UX experts who create and publish "guided interview" web experiences for members of the public and fellow government staff via a friendly browser-based app, no coding necessary. For examples of "guided interview" style web experiences, check out [IRS Direct File](https://coforma.io/case-studies/irs-direct-file#results) (filing your taxes), [GetCalFresh](https://codeforamerica.org/news/overcoming-barriers-setting-expectations-for-calfresh-eligibility/) (Applying for food benefits) and [Court Forms Online](https://courtformsonline.org/) (filing court documents). +- Form Fillers: folks who provide info to the government via guided interviews created by Form Builders ### Things -- **Blueprint**: produced by a content author, the blueprint defines the structure of an interactive session between a court and an SRL -- **Conversation**: one instance of a blueprint; the interactive session between a court and an SRL. Other terms for this concept include dialogue or session. +- **Blueprint**: produced by a form builder, the blueprint defines the structure of an interactive session between a government office and a form filler. +- **Conversation**: one instance of a blueprint; the interactive session between a government office and a form filler. Other terms for this concept include dialogue or session. - **Pattern/template**: the building blocks of a blueprint, patterns implement UX best-practices, defining the content and behavior of the user interface. - **Prompt**: produced by a pattern, the prompt defines what is presented to the end user at single point in a conversation. - **Component**: user interface component that acts as the building block of prompts. @@ -61,6 +61,7 @@ To start developing with hot reloading, use: ```bash pnpm build ``` + then run: ```bash From 5ee1c8c4bf9ab6ac1b6ea2f9b893fbb28bf2b6ea Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Mon, 11 Nov 2024 08:38:54 -0800 Subject: [PATCH 3/3] feat: create email input pattern tckt-362 (#377) * feat: add email input pattern -tckt-362 * feat: add email input pattern form edit tckt-362 * feat: create email input icon for question dropdown tckt-362 * chore: update error message path for dob storybook tckt-362 * test: add unit tests for email-input config file tckt-362 * test: refactor test failure assertions tckt-362 --------- Co-authored-by: kalasgarov --- packages/common/src/locales/en/app.ts | 6 + .../EmailInput/EmailInput.stories.tsx | 58 +++++++++ .../components/EmailInput/EmailInput.test.tsx | 7 ++ .../Form/components/EmailInput/EmailInput.tsx | 37 ++++++ .../src/Form/components/EmailInput/index.tsx | 3 + packages/design/src/Form/components/index.tsx | 6 +- .../FormEdit/AddPatternDropdown.tsx | 27 ++-- .../DateOfBirthPatternEdit.stories.tsx | 2 +- .../EmailInputPatternEdit.stories.tsx | 70 +++++++++++ .../components/EmailInputPatternEdit.test.tsx | 7 ++ .../components/EmailInputPatternEdit.tsx | 92 ++++++++++++++ .../FormManager/FormEdit/components/index.ts | 10 +- .../FormEdit/formEditStyles.module.css | 8 +- .../FormEdit/images/email-icon.svg | 3 + packages/forms/src/components.ts | 8 ++ .../date-of-birth/date-of-birth.test.ts | 8 +- .../patterns/email-input/email-input.test.ts | 119 ++++++++++++++++++ .../src/patterns/email-input/email-input.ts | 97 ++++++++++++++ packages/forms/src/patterns/index.ts | 5 +- 19 files changed, 545 insertions(+), 28 deletions(-) create mode 100644 packages/design/src/Form/components/EmailInput/EmailInput.stories.tsx create mode 100644 packages/design/src/Form/components/EmailInput/EmailInput.test.tsx create mode 100644 packages/design/src/Form/components/EmailInput/EmailInput.tsx create mode 100644 packages/design/src/Form/components/EmailInput/index.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.stories.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.test.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.tsx create mode 100644 packages/design/src/FormManager/FormEdit/images/email-icon.svg create mode 100644 packages/forms/src/patterns/email-input/email-input.test.ts create mode 100644 packages/forms/src/patterns/email-input/email-input.ts diff --git a/packages/common/src/locales/en/app.ts b/packages/common/src/locales/en/app.ts index aa7c383e..54792326 100644 --- a/packages/common/src/locales/en/app.ts +++ b/packages/common/src/locales/en/app.ts @@ -60,5 +60,11 @@ export const en = { hint: 'For example: January 19 2000', errorTextMustContainChar: 'String must contain at least 1 character(s)', }, + emailInput: { + ...defaults, + displayName: 'Email Input label', + fieldLabel: 'Email Input label', + errorTextMustContainChar: 'String must contain at least 1 character(s)', + }, }, }; diff --git a/packages/design/src/Form/components/EmailInput/EmailInput.stories.tsx b/packages/design/src/Form/components/EmailInput/EmailInput.stories.tsx new file mode 100644 index 00000000..281628e1 --- /dev/null +++ b/packages/design/src/Form/components/EmailInput/EmailInput.stories.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { type Meta, type StoryObj } from '@storybook/react'; + +import { EmailInputPattern } from './EmailInput.js'; + +const meta: Meta = { + title: 'patterns/EmailInputPattern', + component: EmailInputPattern, + decorators: [ + (Story, args) => { + const FormDecorator = () => { + const formMethods = useForm({ + defaultValues: { + email: '', + }, + }); + return ( + + + + ); + }; + return ; + }, + ], + tags: ['autodocs'], +}; + +export default meta; + +export const Default: StoryObj = { + args: { + emailId: 'email', + label: 'Email address', + required: true, + }, +}; + +export const WithoutRequired: StoryObj = { + args: { + emailId: 'email', + label: 'Email address', + required: false, + }, +}; + +export const WithError: StoryObj = { + args: { + emailId: 'email', + label: 'Email address with error', + required: true, + error: { + type: 'custom', + message: 'This field has an error', + }, + }, +}; diff --git a/packages/design/src/Form/components/EmailInput/EmailInput.test.tsx b/packages/design/src/Form/components/EmailInput/EmailInput.test.tsx new file mode 100644 index 00000000..a70bb448 --- /dev/null +++ b/packages/design/src/Form/components/EmailInput/EmailInput.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './EmailInput.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/Form/components/EmailInput/EmailInput.tsx b/packages/design/src/Form/components/EmailInput/EmailInput.tsx new file mode 100644 index 00000000..31588db4 --- /dev/null +++ b/packages/design/src/Form/components/EmailInput/EmailInput.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { type EmailInputProps } from '@atj/forms'; +import { type PatternComponent } from '../../index.js'; + +export const EmailInputPattern: PatternComponent = ({ + emailId, + label, + required, + error, +}) => { + const { register } = useFormContext(); + + return ( +
+
+ + +
+ {error && ( + + {error.message} + + )} +
+ ); +}; diff --git a/packages/design/src/Form/components/EmailInput/index.tsx b/packages/design/src/Form/components/EmailInput/index.tsx new file mode 100644 index 00000000..bc02ffc7 --- /dev/null +++ b/packages/design/src/Form/components/EmailInput/index.tsx @@ -0,0 +1,3 @@ +import { EmailInputPattern } from './EmailInput.js'; + +export default EmailInputPattern; diff --git a/packages/design/src/Form/components/index.tsx b/packages/design/src/Form/components/index.tsx index 87d376fa..6bf483f2 100644 --- a/packages/design/src/Form/components/index.tsx +++ b/packages/design/src/Form/components/index.tsx @@ -2,6 +2,8 @@ import { PatternComponent, type ComponentForPattern } from '../index.js'; import Address from './Address/index.js'; import Checkbox from './Checkbox/index.js'; +import DateOfBirth from './DateOfBirth/index.js'; +import EmailInput from './EmailInput/index.js'; import Fieldset from './Fieldset/index.js'; import FormSummary from './FormSummary/index.js'; import PackageDownload from './PackageDownload/index.js'; @@ -12,13 +14,14 @@ import RadioGroup from './RadioGroup/index.js'; import RichText from './RichText/index.js'; import Sequence from './Sequence/index.js'; import SelectDropdown from './SelectDropdown/index.js'; -import DateOfBirth from './DateOfBirth/index.js'; import SubmissionConfirmation from './SubmissionConfirmation/index.js'; import TextInput from './TextInput/index.js'; export const defaultPatternComponents: ComponentForPattern = { address: Address as PatternComponent, checkbox: Checkbox as PatternComponent, + 'date-of-birth': DateOfBirth as PatternComponent, + 'email-input': EmailInput as PatternComponent, fieldset: Fieldset as PatternComponent, 'form-summary': FormSummary as PatternComponent, input: TextInput as PatternComponent, @@ -29,7 +32,6 @@ export const defaultPatternComponents: ComponentForPattern = { 'radio-group': RadioGroup as PatternComponent, 'rich-text': RichText as PatternComponent, 'select-dropdown': SelectDropdown as PatternComponent, - 'date-of-birth': DateOfBirth as PatternComponent, sequence: Sequence as PatternComponent, 'submission-confirmation': SubmissionConfirmation as PatternComponent, }; diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index f542356d..1a33464f 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -10,12 +10,14 @@ import checkboxIcon from './images/checkbox-icon.svg'; import dateIcon from './images/date-icon.svg'; import dropDownIcon from './images/dropdown-icon.svg'; import dropDownOptionIcon from './images/dropdownoption-icon.svg'; -import richTextIcon from './images/richtext-icon.svg'; +import emailInputIcon from './images/email-icon.svg'; import longanswerIcon from './images/longanswer-icon.svg'; import pageIcon from './images/page-icon.svg'; +import richTextIcon from './images/richtext-icon.svg'; import shortanswerIcon from './images/shortanswer-icon.svg'; import singleselectIcon from './images/singleselect-icon.svg'; import templateIcon from './images/template-icon.svg'; + import classNames from 'classnames'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -25,9 +27,10 @@ const icons: Record = { 'date-icon.svg': dateIcon, 'dropdown-icon.svg': dropDownIcon, 'dropdownoption-icon.svg': dropDownOptionIcon, - 'richtext-icon.svg': richTextIcon, + 'email-icon.svg': emailInputIcon, 'longanswer-icon.svg': longanswerIcon, 'page-icon.svg': pageIcon, + 'richtext-icon.svg': richTextIcon, 'shortanswer-icon.svg': shortanswerIcon, 'singleselect-icon.svg': singleselectIcon, 'template-icon.svg': templateIcon, @@ -87,27 +90,29 @@ export const AddPatternMenu = () => { type DropdownPattern = [string, PatternConfig]; const sidebarPatterns: DropdownPattern[] = [ - ['form-summary', defaultFormConfig.patterns['form-summary']], + ['checkbox', defaultFormConfig.patterns['checkbox']], + ['date-of-birth', defaultFormConfig.patterns['date-of-birth']], + ['email-input', defaultFormConfig.patterns['email-input']], ['fieldset', defaultFormConfig.patterns['fieldset']], + ['form-summary', defaultFormConfig.patterns['form-summary']], ['input', defaultFormConfig.patterns['input']], - ['checkbox', defaultFormConfig.patterns['checkbox']], + ['package-download', defaultFormConfig.patterns['package-download']], ['paragraph', defaultFormConfig.patterns['paragraph']], - ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], - ['package-download', defaultFormConfig.patterns['package-download']], + ['rich-text', defaultFormConfig.patterns['rich-text']], ['select-dropdown', defaultFormConfig.patterns['select-dropdown']], - ['date-of-birth', defaultFormConfig.patterns['date-of-birth']], ] as const; export const fieldsetPatterns: DropdownPattern[] = [ + ['checkbox', defaultFormConfig.patterns['checkbox']], + ['date-of-birth', defaultFormConfig.patterns['date-of-birth']], + ['email-input', defaultFormConfig.patterns['email-input']], ['form-summary', defaultFormConfig.patterns['form-summary']], ['input', defaultFormConfig.patterns['input']], - ['checkbox', defaultFormConfig.patterns['checkbox']], + ['package-download', defaultFormConfig.patterns['package-download']], ['paragraph', defaultFormConfig.patterns['paragraph']], - ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], - ['package-download', defaultFormConfig.patterns['package-download']], + ['rich-text', defaultFormConfig.patterns['rich-text']], ['select-dropdown', defaultFormConfig.patterns['select-dropdown']], - ['date-of-birth', defaultFormConfig.patterns['date-of-birth']], ] as const; export const SidebarAddPatternMenuItem = ({ diff --git a/packages/design/src/FormManager/FormEdit/components/DateOfBirthPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/DateOfBirthPatternEdit.stories.tsx index c686994b..8f9acc50 100644 --- a/packages/design/src/FormManager/FormEdit/components/DateOfBirthPatternEdit.stories.tsx +++ b/packages/design/src/FormManager/FormEdit/components/DateOfBirthPatternEdit.stories.tsx @@ -97,7 +97,7 @@ export const Error: StoryObj = { await expect( await canvas.findByText( - message.patterns.selectDropdown.errorTextMustContainChar + message.patterns.dateOfBirth.errorTextMustContainChar ) ).toBeInTheDocument(); }, diff --git a/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.stories.tsx new file mode 100644 index 00000000..e9f3f372 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent } from '@storybook/test'; +import { within } from '@testing-library/react'; + +import { type EmailInputPattern } from '@atj/forms'; +import { createPatternEditStoryMeta } from './common/story-helper.js'; +import FormEdit from '../index.js'; +import { enLocale as message } from '@atj/common'; + +const pattern: EmailInputPattern = { + id: 'email-input-1', + type: 'email-input', + data: { + label: message.patterns.emailInput.displayName, + required: false, + }, +}; + +const storyConfig: Meta = { + title: 'Edit components/EmailInputPattern', + ...createPatternEditStoryMeta({ + pattern, + }), +} as Meta; + +export default storyConfig; + +export const Basic: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const updatedLabel = 'Test Email Update Label'; + + await userEvent.click( + canvas.getByText(message.patterns.emailInput.displayName) + ); + + const labelInput = canvas.getByLabelText( + message.patterns.emailInput.fieldLabel + ); + await userEvent.clear(labelInput); + await userEvent.type(labelInput, updatedLabel); + + const form = labelInput?.closest('form'); + form?.requestSubmit(); + + await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument(); + }, +}; + +export const Error: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click( + canvas.getByText(message.patterns.emailInput.displayName) + ); + + const labelInput = canvas.getByLabelText( + message.patterns.emailInput.fieldLabel + ); + await userEvent.clear(labelInput); + labelInput.blur(); + + await expect( + await canvas.findByText( + message.patterns.emailInput.errorTextMustContainChar + ) + ).toBeInTheDocument(); + }, +}; diff --git a/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.test.tsx b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.test.tsx new file mode 100644 index 00000000..c4df65a2 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './EmailInputPatternEdit.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.tsx new file mode 100644 index 00000000..d738e042 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/EmailInputPatternEdit.tsx @@ -0,0 +1,92 @@ +import classnames from 'classnames'; +import React from 'react'; + +import { type EmailInputProps } from '@atj/forms'; +import { type EmailInputPattern } from '@atj/forms'; + +import EmailInput from '../../../Form/components/EmailInput/index.js'; +import { PatternEditComponent } from '../types.js'; + +import { PatternEditActions } from './common/PatternEditActions.js'; +import { PatternEditForm } from './common/PatternEditForm.js'; +import { usePatternEditFormContext } from './common/hooks.js'; +import { enLocale as message } from '@atj/common'; +import styles from '../formEditStyles.module.css'; + +const EmailInputEdit: PatternEditComponent = ({ + focus, + previewProps, +}) => { + return ( + <> + {focus ? ( + } + > + ) : ( +
+ +
+ )} + + ); +}; + +const EditComponent = ({ pattern }: { pattern: EmailInputPattern }) => { + const { fieldId, getFieldState, register } = + usePatternEditFormContext(pattern.id); + const label = getFieldState('label'); + + return ( +
+
+ +
+
+ + + + + + +
+
+ ); +}; + +export default EmailInputEdit; diff --git a/packages/design/src/FormManager/FormEdit/components/index.ts b/packages/design/src/FormManager/FormEdit/components/index.ts index 4e4f355a..98e05395 100644 --- a/packages/design/src/FormManager/FormEdit/components/index.ts +++ b/packages/design/src/FormManager/FormEdit/components/index.ts @@ -5,12 +5,13 @@ import { import CheckboxPatternEdit from './CheckboxPatternEdit.js'; import DateOfBirthPatternEdit from './DateOfBirthPatternEdit.js'; +import EmailInputPatternEdit from './EmailInputPatternEdit.js'; import FieldsetEdit from './FieldsetEdit.js'; import FormSummaryEdit from './FormSummaryEdit.js'; import InputPatternEdit from './InputPatternEdit.js'; import PackageDownloadPatternEdit from './PackageDownloadPatternEdit.js'; -import { PageEdit } from './PageEdit.js'; import PageSetEdit from './PageSetEdit.js'; +import { PageEdit } from './PageEdit.js'; import ParagraphPatternEdit from './ParagraphPatternEdit.js'; import { PatternPreviewSequence } from './PreviewSequencePattern/index.js'; import RadioGroupPatternEdit from './RadioGroupPatternEdit.js'; @@ -21,13 +22,14 @@ import SubmissionConfirmationEdit from './SubmissionConfirmationEdit.js'; export const defaultPatternEditComponents: EditComponentForPattern = { checkbox: CheckboxPatternEdit as PatternEditComponent, 'date-of-birth': DateOfBirthPatternEdit as PatternEditComponent, - paragraph: ParagraphPatternEdit as PatternEditComponent, - input: InputPatternEdit as PatternEditComponent, - 'form-summary': FormSummaryEdit as PatternEditComponent, + 'email-input': EmailInputPatternEdit as PatternEditComponent, fieldset: FieldsetEdit as PatternEditComponent, + 'form-summary': FormSummaryEdit as PatternEditComponent, + input: InputPatternEdit as PatternEditComponent, 'package-download': PackageDownloadPatternEdit as PatternEditComponent, page: PageEdit as PatternEditComponent, 'page-set': PageSetEdit as PatternEditComponent, + paragraph: ParagraphPatternEdit as PatternEditComponent, 'radio-group': RadioGroupPatternEdit as PatternEditComponent, 'rich-text': RichTextPatternEdit as PatternEditComponent, 'select-dropdown': SelectDropdownPatternEdit as PatternEditComponent, diff --git a/packages/design/src/FormManager/FormEdit/formEditStyles.module.css b/packages/design/src/FormManager/FormEdit/formEditStyles.module.css index 36f46de2..96e5c258 100644 --- a/packages/design/src/FormManager/FormEdit/formEditStyles.module.css +++ b/packages/design/src/FormManager/FormEdit/formEditStyles.module.css @@ -51,11 +51,9 @@ padding-left: 1.5rem; } -.draggableListItemWrapper .radioFormPattern legend { - padding-left: 0; -} - -.draggableListItemWrapper .dateOfBirthPattern legend { +.draggableListItemWrapper .radioFormPattern legend, +.draggableListItemWrapper .dateOfBirthPattern legend, +.draggableListItemWrapper .emailInput legend { padding-left: 0; } diff --git a/packages/design/src/FormManager/FormEdit/images/email-icon.svg b/packages/design/src/FormManager/FormEdit/images/email-icon.svg new file mode 100644 index 00000000..46bc2d51 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/images/email-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index a4f4b5da..b5c65a95 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -112,6 +112,14 @@ export type DateOfBirthProps = PatternProps<{ error?: FormError; }>; +export type EmailInputProps = PatternProps<{ + type: 'email-input'; + emailId: string; + label: string; + required: boolean; + error?: FormError; +}>; + export type SequenceProps = PatternProps<{ type: 'sequence'; }>; diff --git a/packages/forms/src/patterns/date-of-birth/date-of-birth.test.ts b/packages/forms/src/patterns/date-of-birth/date-of-birth.test.ts index 15d4b34a..77204553 100644 --- a/packages/forms/src/patterns/date-of-birth/date-of-birth.test.ts +++ b/packages/forms/src/patterns/date-of-birth/date-of-birth.test.ts @@ -58,7 +58,7 @@ describe('DateOfBirthPattern tests', () => { if (result.success) { expect(result.data).toEqual(inputValue); } else { - throw new Error('Unexpected validation failure'); + expect.fail('Unexpected validation failure'); } }); @@ -81,7 +81,7 @@ describe('DateOfBirthPattern tests', () => { if (!result.success) { expect(result.error).toBeDefined(); } else { - throw new Error('Unexpected validation success'); + expect.fail('Unexpected validation success'); } }); @@ -101,7 +101,7 @@ describe('DateOfBirthPattern tests', () => { expect(result.data.required).toBe(true); expect(result.data.hint).toBe('Enter your date of birth'); } else { - throw new Error('Unexpected validation failure'); + expect.fail('Unexpected validation failure'); } }); @@ -119,7 +119,7 @@ describe('DateOfBirthPattern tests', () => { if (!result.success) { expect(result.error).toBeDefined(); } else { - throw new Error('Unexpected validation success'); + expect.fail('Unexpected validation success'); } }); }); diff --git a/packages/forms/src/patterns/email-input/email-input.test.ts b/packages/forms/src/patterns/email-input/email-input.test.ts new file mode 100644 index 00000000..4c984d21 --- /dev/null +++ b/packages/forms/src/patterns/email-input/email-input.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest'; +import { + createEmailSchema, + emailInputConfig, + type EmailInputPattern, +} from './email-input'; + +describe('EmailInputPattern tests', () => { + describe('createEmailSchema', () => { + it('should create schema for required email input', () => { + const data: EmailInputPattern['data'] = { + label: 'Test Email Input Label', + required: true, + }; + + const schema = createEmailSchema(data); + const validInput = { email: 'testEmail@test.com' }; + const invalidInput = { email: 'testEmail.com' }; + + expect(schema.safeParse(validInput).success).toBe(true); + expect(schema.safeParse(invalidInput).success).toBe(false); + }); + + it('should create schema for optional email input', () => { + const data: EmailInputPattern['data'] = { + label: 'Test Email Input Label', + required: false, + }; + + const schema = createEmailSchema(data); + const validInput = { email: 'testEmail@test.com' }; + const emptyInput = {}; + + expect(schema.safeParse(validInput).success).toBe(true); + expect(schema.safeParse(emptyInput).success).toBe(true); + }); + }); + + describe('emailInputConfig', () => { + it('should parse user input correctly', () => { + const pattern: EmailInputPattern = { + type: 'email-input', + id: 'test', + data: { + label: 'Test Email Input Label', + required: true, + }, + }; + + const inputValue = { email: 'testEmail@test.com' }; + if (!emailInputConfig.parseUserInput) { + expect.fail('emailInputConfig.parseUserInput is undefined'); + } + const result = emailInputConfig.parseUserInput(pattern, inputValue); + if (result.success) { + expect(result.data).toEqual(inputValue); + } else { + expect.fail('Unexpected validation failure'); + } + }); + + it('should handle validation error for user input', () => { + const pattern: EmailInputPattern = { + type: 'email-input', + id: 'test', + data: { + label: 'Test Email Input Label', + required: true, + }, + }; + + const inputValue = { email: 'testEmail.co' }; + if (!emailInputConfig.parseUserInput) { + expect.fail('emailInputConfig.parseUserInput is undefined'); + } + const result = emailInputConfig.parseUserInput(pattern, inputValue); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + expect.fail('Unexpected validation success'); + } + }); + + it('should parse config data correctly', () => { + const obj = { + label: 'Test Email Input Label', + required: true, + }; + + if (!emailInputConfig.parseConfigData) { + expect.fail('emailInputConfig.parseConfigData is undefined'); + } + const result = emailInputConfig.parseConfigData(obj); + if (result.success) { + expect(result.data.label).toBe('Test Email Input Label'); + expect(result.data.required).toBe(true); + } else { + expect.fail('Unexpected validation failure'); + } + }); + + it('should handle invalid config data', () => { + const obj = { + label: '', + required: true, + }; + + if (!emailInputConfig.parseConfigData) { + expect.fail('emailInputConfig.parseConfigData is undefined'); + } + const result = emailInputConfig.parseConfigData(obj); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + expect.fail('Unexpected validation success'); + } + }); + }); +}); diff --git a/packages/forms/src/patterns/email-input/email-input.ts b/packages/forms/src/patterns/email-input/email-input.ts new file mode 100644 index 00000000..1572706f --- /dev/null +++ b/packages/forms/src/patterns/email-input/email-input.ts @@ -0,0 +1,97 @@ +import * as z from 'zod'; + +import { type EmailInputProps } from '../../components.js'; +import { + type Pattern, + type PatternConfig, + validatePattern, +} from '../../pattern.js'; +import { getFormSessionValue } from '../../session.js'; +import { + safeZodParseFormErrors, + safeZodParseToFormError, +} from '../../util/zod.js'; + +const configSchema = z.object({ + label: z.string().min(1), + required: z.boolean(), +}); + +export type EmailInputPattern = Pattern>; + +export type EmailInputPatternOutput = z.infer< + ReturnType +>; + +export const createEmailSchema = (data: EmailInputPattern['data']) => { + const emailSchema = z + .string() + .regex( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + 'Invalid email format' + ) + .optional(); + + if (!data.required) { + return z + .object({ + email: emailSchema, + }) + .optional(); + } + + return z.object({ + email: emailSchema, + }); +}; + +export const emailInputConfig: PatternConfig< + EmailInputPattern, + EmailInputPatternOutput +> = { + displayName: 'Email Input', + iconPath: 'email-icon.svg', + initial: { + label: 'Email Input', + required: true, + }, + + parseUserInput: (pattern, inputValue) => { + console.log('TEST Parsing user input:', inputValue); + return safeZodParseToFormError(createEmailSchema(pattern.data), inputValue); + }, + + parseConfigData: obj => { + return safeZodParseFormErrors(configSchema, obj); + }, + getChildren() { + return []; + }, + + createPrompt(_, session, pattern, options) { + const extraAttributes: Record = {}; + const sessionValue = getFormSessionValue(session, pattern.id); + if (options.validate) { + const isValidResult = validatePattern( + emailInputConfig, + pattern, + sessionValue + ); + if (!isValidResult.success) { + extraAttributes['error'] = isValidResult.error; + } + } + + return { + props: { + _patternId: pattern.id, + type: 'email-input', + label: pattern.data.label, + emailId: `${pattern.id}.email`, + required: pattern.data.required, + ...extraAttributes, + } as EmailInputProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index 4fe5dbcd..05ae1c7c 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -3,6 +3,7 @@ import { type FormConfig } from '../pattern.js'; import { addressConfig } from './address/index.js'; import { checkboxConfig } from './checkbox.js'; import { dateOfBirthConfig } from './date-of-birth/date-of-birth.js'; +import { emailInputConfig } from './email-input/email-input.js'; import { fieldsetConfig } from './fieldset/index.js'; import { formSummaryConfig } from './form-summary.js'; import { inputConfig } from './input/index.js'; @@ -23,6 +24,7 @@ export const defaultFormConfig: FormConfig = { address: addressConfig, checkbox: checkboxConfig, 'date-of-birth': dateOfBirthConfig, + 'email-input': emailInputConfig, fieldset: fieldsetConfig, 'form-summary': formSummaryConfig, input: inputConfig, @@ -30,8 +32,8 @@ export const defaultFormConfig: FormConfig = { page: pageConfig, 'page-set': pageSetConfig, paragraph: paragraphConfig, - 'rich-text': richTextConfig, 'radio-group': radioGroupConfig, + 'rich-text': richTextConfig, 'select-dropdown': selectDropdownConfig, sequence: sequenceConfig, }, @@ -40,6 +42,7 @@ export const defaultFormConfig: FormConfig = { export * from './address/index.js'; export * from './checkbox.js'; export * from './date-of-birth/date-of-birth.js'; +export * from './email-input/email-input.js'; export * from './fieldset/index.js'; export { type FieldsetPattern } from './fieldset/config.js'; export * from './form-summary.js';