From 4e15c9ed845c9cf0173a849cf644c15f01804229 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Wed, 30 Oct 2024 10:40:03 -0700 Subject: [PATCH 1/9] feat: create select dropdown pattern tckt-360 --- packages/common/src/locales/en/app.ts | 6 + .../SelectDropdown/SelectDropdown.stories.tsx | 77 +++++++++++ .../SelectDropdown/SelectDropdown.test.tsx | 7 + .../SelectDropdown/SelectDropdown.tsx | 38 ++++++ .../Form/components/SelectDropdown/index.tsx | 3 + packages/design/src/Form/components/index.tsx | 2 + .../FormEdit/AddPatternDropdown.tsx | 2 + .../FormManager/FormEdit/components/index.ts | 2 + packages/forms/src/components.ts | 12 ++ packages/forms/src/documents/document.ts | 11 ++ packages/forms/src/documents/pdf/generate.ts | 3 + packages/forms/src/documents/pdf/index.ts | 3 +- .../forms/src/documents/pdf/parsing-api.ts | 9 +- packages/forms/src/documents/types.ts | 10 ++ packages/forms/src/patterns/index.ts | 3 + .../forms/src/patterns/select-dropdown.ts | 121 ++++++++++++++++++ 16 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx create mode 100644 packages/design/src/Form/components/SelectDropdown/SelectDropdown.test.tsx create mode 100644 packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx create mode 100644 packages/design/src/Form/components/SelectDropdown/index.tsx create mode 100644 packages/forms/src/patterns/select-dropdown.ts diff --git a/packages/common/src/locales/en/app.ts b/packages/common/src/locales/en/app.ts index 25dd6ccb..956dfc15 100644 --- a/packages/common/src/locales/en/app.ts +++ b/packages/common/src/locales/en/app.ts @@ -46,5 +46,11 @@ export const en = { fieldLabel: 'Radio group label', errorTextMustContainChar: 'String must contain at least 1 character(s)', }, + selectDropdown: { + ...defaults, + displayName: 'Select Dropdown label', + fieldLabel: 'Select Dropdown label', + errorTextMustContainChar: 'String must contain at least 1 character(s)', + }, }, }; diff --git a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx new file mode 100644 index 00000000..4e6468b4 --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { type Meta, type StoryObj } from '@storybook/react'; + +import { SelectDropdownPattern } from './SelectDropdown.js'; + +const meta: Meta = { + title: 'patterns/SelectPattern', + component: SelectDropdownPattern, + decorators: [ + (Story, args) => { + const FormDecorator = () => { + const formMethods = useForm(); + return ( + + + + ); + }; + return ; + }, + ], + tags: ['autodocs'], +}; + +export default meta; +export const Default: StoryObj = { + args: { + _patternId: '', + type: 'select-dropdown', + selectId: 'select-1', + label: 'Select an option', + required: false, + options: [ + { value: 'default', label: '- Select -' }, + { value: 'value1', label: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, + ], + }, +}; + +export const WithError: StoryObj = { + args: { + _patternId: '', + type: 'select-dropdown', + selectId: 'select-with-error', + label: 'Select an option with error', + required: false, + options: [ + { value: 'default', label: '- Select -' }, + { value: 'value1', label: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, + ], + error: { + type: 'custom', + message: 'This field has an error', + }, + }, +}; + +export const Required: StoryObj = { + args: { + _patternId: '', + type: 'select-dropdown', + selectId: 'select-required', + label: 'Select a required option', + required: true, + options: [ + { value: 'value1', label: '- Select -' }, + { value: 'value2', label: 'Option-1' }, + { value: 'value3', label: 'Option-2' }, + { value: 'value4', label: 'Option-3' }, + ], + }, +}; diff --git a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.test.tsx b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.test.tsx new file mode 100644 index 00000000..29006e23 --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './SelectDropdown.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx new file mode 100644 index 00000000..5a6b5632 --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { type SelectDropdownProps } from '@atj/forms'; +import { type PatternComponent } from '../../index.js'; + +export const SelectDropdownPattern: PatternComponent = ({ + selectId, + label, + required, + options, + error, +}) => { + const { register } = useFormContext(); + return ( +
+
+ + + {error && ( + + {error.message} + + )} +
+
+ ); +}; diff --git a/packages/design/src/Form/components/SelectDropdown/index.tsx b/packages/design/src/Form/components/SelectDropdown/index.tsx new file mode 100644 index 00000000..6fb82928 --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/index.tsx @@ -0,0 +1,3 @@ +import { SelectDropdownPattern } from './SelectDropdown.js'; + +export default SelectDropdownPattern; diff --git a/packages/design/src/Form/components/index.tsx b/packages/design/src/Form/components/index.tsx index 08e4e433..6c63a7d4 100644 --- a/packages/design/src/Form/components/index.tsx +++ b/packages/design/src/Form/components/index.tsx @@ -11,6 +11,7 @@ import Paragraph from './Paragraph/index.js'; 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 SubmissionConfirmation from './SubmissionConfirmation/index.js'; import TextInput from './TextInput/index.js'; @@ -26,6 +27,7 @@ export const defaultPatternComponents: ComponentForPattern = { paragraph: Paragraph as PatternComponent, 'radio-group': RadioGroup as PatternComponent, 'rich-text': RichText as PatternComponent, + 'select-dropdown': SelectDropdown 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 2abeb996..129048cf 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -95,6 +95,7 @@ const sidebarPatterns: DropdownPattern[] = [ ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], ['package-download', defaultFormConfig.patterns['package-download']], + ['select-dropdown', defaultFormConfig.patterns['select-dropdown']], ] as const; export const fieldsetPatterns: DropdownPattern[] = [ ['form-summary', defaultFormConfig.patterns['form-summary']], @@ -104,6 +105,7 @@ export const fieldsetPatterns: DropdownPattern[] = [ ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], ['package-download', defaultFormConfig.patterns['package-download']], + ['select-dropdown', defaultFormConfig.patterns['select-dropdown']], ] as const; export const SidebarAddPatternMenuItem = ({ diff --git a/packages/design/src/FormManager/FormEdit/components/index.ts b/packages/design/src/FormManager/FormEdit/components/index.ts index 2e8fa369..44a5934b 100644 --- a/packages/design/src/FormManager/FormEdit/components/index.ts +++ b/packages/design/src/FormManager/FormEdit/components/index.ts @@ -14,6 +14,7 @@ import ParagraphPatternEdit from './ParagraphPatternEdit.js'; import { PatternPreviewSequence } from './PreviewSequencePattern/index.js'; import RadioGroupPatternEdit from './RadioGroupPatternEdit.js'; import RichTextPatternEdit from './RichTextPatternEdit/index.js'; +import SelectDropdownPatternEdit from './SelectDropdownPatternEdit.js'; import SubmissionConfirmationEdit from './SubmissionConfirmationEdit.js'; export const defaultPatternEditComponents: EditComponentForPattern = { @@ -27,6 +28,7 @@ export const defaultPatternEditComponents: EditComponentForPattern = { 'page-set': PageSetEdit as PatternEditComponent, 'radio-group': RadioGroupPatternEdit as PatternEditComponent, 'rich-text': RichTextPatternEdit as PatternEditComponent, + 'select-dropdown': SelectDropdownPatternEdit as PatternEditComponent, sequence: PatternPreviewSequence as PatternEditComponent, 'submission-confirmation': SubmissionConfirmationEdit as PatternEditComponent, }; diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index 4f074a97..7c34bcc9 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -89,6 +89,18 @@ export type RadioGroupProps = PatternProps<{ }[]; }>; +export type SelectDropdownProps = PatternProps<{ + type: 'select-dropdown'; + selectId: string; + options: { + value: string; + label: string; + }[]; + label: string; + required: boolean; + error?: FormError; +}>; + export type SequenceProps = PatternProps<{ type: 'sequence'; }>; diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index 2e455702..30f9507f 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -137,6 +137,17 @@ export const addDocumentFieldsToForm = ( maxLength: 128, }, } satisfies InputPattern); + } else if (field.type === 'SelectDropdown') { + patterns.push({ + type: 'input', + id: patternId, + data: { + label: field.label, + initial: '', + required: false, + maxLength: 128, + }, + } satisfies InputPattern); } else if (field.type === 'Paragraph' || field.type === 'RichText') { // skip purely presentational fields } else if (field.type === 'not-supported') { diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index 0750d87c..50c98c17 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -113,6 +113,9 @@ const setFormFieldData = ( } else if (fieldType === 'OptionList') { const field = form.getDropdown(fieldName); field.select(fieldValue); + } else if (fieldType === 'SelectDropdown') { + const field = form.getDropdown(fieldName); + field.select(fieldValue); } else if (fieldType === 'RadioGroup') { // TODO: remove this when we have a better way to handle radio groups try { diff --git a/packages/forms/src/documents/pdf/index.ts b/packages/forms/src/documents/pdf/index.ts index 6cdd0aba..9a74b5b1 100644 --- a/packages/forms/src/documents/pdf/index.ts +++ b/packages/forms/src/documents/pdf/index.ts @@ -19,4 +19,5 @@ export type PDFFieldType = | 'OptionList' | 'RadioGroup' | 'Paragraph' - | 'RichText'; + | 'RichText' + | 'SelectDropdown'; diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index 82d44f15..e77749b3 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -83,7 +83,14 @@ const ExtractedObject = z.object({ raw_text: z.string(), form_summary: FormSummary, elements: z - .union([TxInput, Checkbox, RadioGroup, Paragraph, Fieldset, RichText]) + .union([ + TxInput, + Checkbox, + RadioGroup, + Paragraph, + Fieldset, + RichText, + ]) .array(), }); diff --git a/packages/forms/src/documents/types.ts b/packages/forms/src/documents/types.ts index a94d4d2a..fad1ad0f 100644 --- a/packages/forms/src/documents/types.ts +++ b/packages/forms/src/documents/types.ts @@ -36,6 +36,16 @@ export type DocumentFieldValue = value: string; required: boolean; } + | { + type: 'SelectDropdown'; + name: string; + options: { + value: string; + label: string; + }[]; + label: string; + required: boolean; + } | { type: 'Paragraph'; name: string; diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index f502ffc4..11efe36b 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -11,6 +11,7 @@ import { pageSetConfig } from './page-set/index.js'; import { paragraphConfig } from './paragraph.js'; import { radioGroupConfig } from './radio-group.js'; import { richTextConfig } from './rich-text.js'; +import { selectDropdownConfig } from './select-dropdown.js'; import { sequenceConfig } from './sequence.js'; // This configuration reflects what a user of this library would provide for @@ -29,6 +30,7 @@ export const defaultFormConfig: FormConfig = { paragraph: paragraphConfig, 'rich-text': richTextConfig, 'radio-group': radioGroupConfig, + 'select-dropdown': selectDropdownConfig, sequence: sequenceConfig, }, } as const; @@ -47,4 +49,5 @@ export * from './page-set/index.js'; export { type PageSetPattern } from './page-set/config.js'; export * from './paragraph.js'; export * from './radio-group.js'; +export * from './select-dropdown.js'; export * from './sequence.js'; diff --git a/packages/forms/src/patterns/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown.ts new file mode 100644 index 00000000..b3f5e387 --- /dev/null +++ b/packages/forms/src/patterns/select-dropdown.ts @@ -0,0 +1,121 @@ +import * as z from 'zod'; + +import { type SelectDropdownProps } from '../components.js'; +import { + type Pattern, + type PatternConfig, + validatePattern, +} from '../pattern.js'; +import { getFormSessionValue } from '../session.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; + +const configSchema = z.object({ + label: z.string().min(1), + required: z.boolean(), + options: z + .object({ + value: z + .string() + .regex(/^[A-Za-z][A-Za-z0-9\-_:.]*$/, 'Invalid Option Value'), + label: z.string().min(1), + }) + .array(), +}); +export type SelectDropdownPattern = Pattern>; + +const PatternOutput = z.string(); +type PatternOutput = z.infer; + +export const selectDropdownConfig: PatternConfig< + SelectDropdownPattern, + PatternOutput +> = { + displayName: 'Select Dropdown', + iconPath: 'dropdown-icon.svg', + initial: { + label: 'Select-dropdown-label', + required: true, + options: [ + { value: 'value1', label: '-Select-' }, + { value: 'value2', label: 'Option-1' }, + { value: 'value3', label: 'Option-2' }, + { value: 'value4', label: 'Option-3' }, + ], + }, + + parseUserInput: (pattern, input: unknown) => { + console.log('TEST parseUserInput'); + + // FIXME: Not sure why we're sometimes getting a string here, and sometimes + // the expected object. Workaround, by accepting both. + if (typeof input === 'string') { + return { + success: true, + data: input, + }; + } + // const optionId = getSelectedOption(pattern, input); + return { + success: true, + data: '', + }; + /* + if (optionId) { + return { + success: true, + data: optionId, + }; + } + return { + success: false, + error: { + type: 'custom', + message: `No option selected for radio group: ${pattern.id}. Input: ${input}`, + }, + }; + */ + }, + + parseConfigData: obj => { + const result = safeZodParseFormErrors(configSchema, obj); + console.log('TEST ParseConfigData', result); + + return result; + }, + getChildren() { + return []; + }, + + // QUESTION: where are we using this? + createPrompt(_, session, pattern, options) { + const extraAttributes: Record = {}; + const sessionValue = getFormSessionValue(session, pattern.id); + if (options.validate) { + const isValidResult = validatePattern( + selectDropdownConfig, + pattern, + sessionValue + ); + if (!isValidResult.success) { + extraAttributes['error'] = isValidResult.error; + } + } + return { + props: { + _patternId: pattern.id, + type: 'select-dropdown', + label: pattern.data.label, + selectId: pattern.id, + options: pattern.data.options.map(option => { + return { + value: option.value, + label: option.label, + }; + }), + required: pattern.data.required, + ...extraAttributes, + } as SelectDropdownProps, + children: [], + }; + }, +}; From 8edc7b0d8780fc0fca94c9fbf10e37fa35f0f3df Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Wed, 30 Oct 2024 10:41:11 -0700 Subject: [PATCH 2/9] feat: create select dropdown pattern edit for form builder tckt-360 --- .../SelectDropdownPatternEdit.stories.tsx | 130 ++++++++++++++++ .../SelectDropdownPatternEdit.test.tsx | 7 + .../components/SelectDropdownPatternEdit.tsx | 144 ++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.test.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx diff --git a/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx new file mode 100644 index 00000000..b43811e3 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx @@ -0,0 +1,130 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { type SelectDropdownPattern } from '@atj/forms'; + +import { createPatternEditStoryMeta } from './common/story-helper.js'; +import FormEdit from '../index.js'; +import CheckboxPatternEdit from './CheckboxPatternEdit.js'; +import { enLocale as message } from '@atj/common'; +import { expect, userEvent } from '@storybook/test'; +import { within } from '@testing-library/react'; + +const pattern: SelectDropdownPattern = { + id: 'select-dropdown-1', + type: 'select-dropdown', + data: { + label: message.patterns.selectDropdown.displayName, + required: false, + options: [ + { value: 'value1', label: '- select -' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, + { value: 'value4', label: 'Option-4' }, + ], + }, +}; + +const storyConfig: Meta = { + title: 'Edit components/SelectDropdownPattern', + ...createPatternEditStoryMeta({ + pattern, + }), +} as Meta; +export default storyConfig; + +export const Basic: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const updatedLabel = 'Select Dropdown update'; + + await userEvent.click( + canvas.getByText(message.patterns.selectDropdown.displayName) + ); + const input = canvas.getByLabelText( + message.patterns.selectDropdown.fieldLabel + ); + const optionLabel = canvas.getByLabelText('Option 1 label'); + + // Enter new text for the field + await userEvent.clear(input); + await userEvent.type(input, updatedLabel); + await userEvent.clear(optionLabel); + await userEvent.type(optionLabel, '- Select an option -'); + + const form = input?.closest('form'); + /** + * The key behavior outside of Storybook submits the form, which commits the pending edit. + * Here, we want to simulate the keypress in the story since Storybook manipulates + * the default behavior and does not register the enter key if it's in the `userEvent.type` function arg. + */ + form?.requestSubmit(); + + await expect(await canvas.findByText(updatedLabel)).toBeInTheDocument(); + await expect(await canvas.findByText('- Select an option -')).toBeVisible(); + }, +}; + +export const AddField: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click( + canvas.getByText(message.patterns.selectDropdown.displayName) + ); + + await userEvent.click( + canvas.getByRole('button', { + name: /add new/i, + }) + ); + + await expect( + await canvas.findByLabelText('Option 4 label') + ).toBeInTheDocument(); + }, +}; + +export const Error: StoryObj = { + play: async ({ canvasElement }) => { + userEvent.setup(); + + const canvas = within(canvasElement); + + await userEvent.click( + canvas.getByText(message.patterns.selectDropdown.displayName) + ); + + const input = canvas.getByLabelText( + message.patterns.selectDropdown.fieldLabel + ); + const optionLabel = canvas.getByLabelText('Option 1 label'); + + // Clear input, remove focus, and wait for error + await userEvent.clear(input); + input.blur(); + + await expect( + await canvas.findByText( + message.patterns.selectDropdown.errorTextMustContainChar + ) + ).toBeInTheDocument(); + + /* + Repopulate the input value since the error text is indistinguishable from + the error text from the option label below + */ + await userEvent.type(input, message.patterns.selectDropdown.fieldLabel); + + await userEvent.clear(optionLabel); + optionLabel.blur(); + + await expect( + await canvas.findByText( + message.patterns.selectDropdown.errorTextMustContainChar + ) + ).toBeInTheDocument(); + + await userEvent.clear(input); + input.blur(); + }, +}; diff --git a/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.test.tsx b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.test.tsx new file mode 100644 index 00000000..08f7ef2f --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './SelectDropdownPatternEdit.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx new file mode 100644 index 00000000..39eadeb1 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx @@ -0,0 +1,144 @@ +import classnames from 'classnames'; +import React, { useState } from 'react'; + +import { type SelectDropdownProps } from '@atj/forms'; +import { type SelectDropdownPattern } from '@atj/forms'; + +import SelectDropdown from '../../../Form/components/SelectDropdown/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 SelectDropdownPatternEdit: PatternEditComponent = ({ + focus, + previewProps, +}) => { + return ( + <> + {focus ? ( + } + > + ) : ( +
+ +
+ )} + + ); +}; + +const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => { + const { fieldId, getFieldState, register } = + usePatternEditFormContext(pattern.id); + const [options, setOptions] = useState(pattern.data.options); + const label = getFieldState('label'); + + return ( +
+
+ +
+
+ {options.map((option, index) => { + const optionValue = getFieldState(`options.${index}.value`); + const optionLabel = getFieldState(`options.${index}.label`); + return ( +
+ {optionValue.error ? ( + + {optionValue.error.message} + + ) : null} + {optionLabel.error ? ( + + {optionLabel.error.message} + + ) : null} +
+ + +
+
+ ); + })} + +
+
+ + + + + + +
+
+ ); +}; + +export default SelectDropdownPatternEdit; From c99d5c9a88ee790aa7cec22eae66a7482b95517d Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Wed, 30 Oct 2024 14:49:11 -0700 Subject: [PATCH 3/9] feat: refactor rendering default select option via the template tckt-360 --- .../SelectDropdown/SelectDropdown.stories.tsx | 9 +-- .../SelectDropdown/SelectDropdown.tsx | 4 +- .../SelectDropdownPatternEdit.stories.tsx | 5 +- .../components/SelectDropdownPatternEdit.tsx | 8 +-- .../forms/src/patterns/select-dropdown.ts | 57 +++++-------------- 5 files changed, 26 insertions(+), 57 deletions(-) diff --git a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx index 4e6468b4..51fe214c 100644 --- a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx @@ -32,7 +32,6 @@ export const Default: StoryObj = { label: 'Select an option', required: false, options: [ - { value: 'default', label: '- Select -' }, { value: 'value1', label: 'Option-1' }, { value: 'value2', label: 'Option-2' }, { value: 'value3', label: 'Option-3' }, @@ -48,7 +47,6 @@ export const WithError: StoryObj = { label: 'Select an option with error', required: false, options: [ - { value: 'default', label: '- Select -' }, { value: 'value1', label: 'Option-1' }, { value: 'value2', label: 'Option-2' }, { value: 'value3', label: 'Option-3' }, @@ -68,10 +66,9 @@ export const Required: StoryObj = { label: 'Select a required option', required: true, options: [ - { value: 'value1', label: '- Select -' }, - { value: 'value2', label: 'Option-1' }, - { value: 'value3', label: 'Option-2' }, - { value: 'value4', label: 'Option-3' }, + { value: 'value1', label: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, ], }, }; diff --git a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx index 5a6b5632..188e3bd8 100644 --- a/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx @@ -20,7 +20,9 @@ export const SelectDropdownPattern: PatternComponent = ({ {required && *} @@ -108,8 +108,8 @@ const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => { type="button" onClick={event => { event.preventDefault(); - const optionId = `option-${options.length + 1}`; - const optionValue = `value-${options.length + 1}`; + const optionId = `option-${options.length}`; + const optionValue = `value-${options.length}`; setOptions(options.concat({ value: optionValue, label: optionId })); }} > diff --git a/packages/forms/src/patterns/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown.ts index b3f5e387..3d6f475b 100644 --- a/packages/forms/src/patterns/select-dropdown.ts +++ b/packages/forms/src/patterns/select-dropdown.ts @@ -7,7 +7,10 @@ import { validatePattern, } from '../pattern.js'; import { getFormSessionValue } from '../session.js'; -import { safeZodParseFormErrors } from '../util/zod.js'; +import { + safeZodParseFormErrors, + safeZodParseToFormError, +} from '../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), @@ -21,14 +24,15 @@ const configSchema = z.object({ }) .array(), }); + export type SelectDropdownPattern = Pattern>; -const PatternOutput = z.string(); -type PatternOutput = z.infer; +const SelectDropdownSchema = z.string(); +type SelectDropdownPatternOutput = z.infer; export const selectDropdownConfig: PatternConfig< SelectDropdownPattern, - PatternOutput + SelectDropdownPatternOutput > = { displayName: 'Select Dropdown', iconPath: 'dropdown-icon.svg', @@ -36,57 +40,24 @@ export const selectDropdownConfig: PatternConfig< label: 'Select-dropdown-label', required: true, options: [ - { value: 'value1', label: '-Select-' }, - { value: 'value2', label: 'Option-1' }, - { value: 'value3', label: 'Option-2' }, - { value: 'value4', label: 'Option-3' }, + { value: 'value1', label: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, ], }, - - parseUserInput: (pattern, input: unknown) => { - console.log('TEST parseUserInput'); - - // FIXME: Not sure why we're sometimes getting a string here, and sometimes - // the expected object. Workaround, by accepting both. - if (typeof input === 'string') { - return { - success: true, - data: input, - }; - } - // const optionId = getSelectedOption(pattern, input); - return { - success: true, - data: '', - }; - /* - if (optionId) { - return { - success: true, - data: optionId, - }; - } - return { - success: false, - error: { - type: 'custom', - message: `No option selected for radio group: ${pattern.id}. Input: ${input}`, - }, - }; - */ + // STILL IN PROGRESS: + parseUserInput: (_, inputObj) => { + return safeZodParseToFormError(SelectDropdownSchema, inputObj); }, parseConfigData: obj => { const result = safeZodParseFormErrors(configSchema, obj); - console.log('TEST ParseConfigData', result); - return result; }, getChildren() { return []; }, - // QUESTION: where are we using this? createPrompt(_, session, pattern, options) { const extraAttributes: Record = {}; const sessionValue = getFormSessionValue(session, pattern.id); From 075a01d7eba0d67c88097bd5ebd1249ab299202e Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Thu, 31 Oct 2024 12:03:43 -0700 Subject: [PATCH 4/9] fix: parseUserInput hook for select dropdown tckt-360 --- .../components/SelectDropdownPatternEdit.tsx | 8 +-- .../forms/src/patterns/select-dropdown.ts | 56 ++++++++++++++++--- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx index f1cd5d26..39eadeb1 100644 --- a/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.tsx @@ -90,14 +90,14 @@ const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => { id={fieldId(`options.${index}.value`)} {...register(`options.${index}.value`)} defaultValue={option.value} - aria-label={`Option ${index} value`} + aria-label={`Option ${index + 1} value`} /> @@ -108,8 +108,8 @@ const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => { type="button" onClick={event => { event.preventDefault(); - const optionId = `option-${options.length}`; - const optionValue = `value-${options.length}`; + const optionId = `option-${options.length + 1}`; + const optionValue = `value-${options.length + 1}`; setOptions(options.concat({ value: optionValue, label: optionId })); }} > diff --git a/packages/forms/src/patterns/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown.ts index 3d6f475b..8c7e2216 100644 --- a/packages/forms/src/patterns/select-dropdown.ts +++ b/packages/forms/src/patterns/select-dropdown.ts @@ -7,10 +7,7 @@ import { validatePattern, } from '../pattern.js'; import { getFormSessionValue } from '../session.js'; -import { - safeZodParseFormErrors, - safeZodParseToFormError, -} from '../util/zod.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), @@ -27,8 +24,24 @@ const configSchema = z.object({ export type SelectDropdownPattern = Pattern>; -const SelectDropdownSchema = z.string(); -type SelectDropdownPatternOutput = z.infer; +type SelectDropdownPatternOutput = string; +export type InputPatternOutput = z.infer>; + +export const createSchema = (data: SelectDropdownPattern['data']) => { + const values = data.options.map(option => option.value); + + if (values.length === 0) { + throw new Error('Options must have at least one value'); + } + + const schema = z.enum([values[0], ...values.slice(1)]); + + if (!data.required) { + return z.union([schema, z.literal('')]).transform(val => val || undefined); + } + + return schema; +}; export const selectDropdownConfig: PatternConfig< SelectDropdownPattern, @@ -45,9 +58,34 @@ export const selectDropdownConfig: PatternConfig< { value: 'value3', label: 'Option-3' }, ], }, - // STILL IN PROGRESS: - parseUserInput: (_, inputObj) => { - return safeZodParseToFormError(SelectDropdownSchema, inputObj); + + parseUserInput: (pattern, inputObj) => { + const expectedInput = inputObj as { value: string }; + + const schema = createSchema(pattern.data); + try { + const parsedValue = schema.parse(expectedInput.value); + return parsedValue + ? { success: true, data: parsedValue } + : { + success: false, + error: { + type: 'custom', + message: 'Parsed select dropdown value is undefined', + }, + }; + } catch (e) { + const zodError = e as z.ZodError; + return { + success: false, + error: { + type: 'custom', + message: zodError.errors + ? zodError.errors[0].message + : zodError.message, + }, + }; + } }, parseConfigData: obj => { From d8f9f9578391a732e69a945c0f112be119944b48 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Thu, 31 Oct 2024 12:06:04 -0700 Subject: [PATCH 5/9] test: add unit tests for select dropdown pattern tckt-360 --- packages/forms/tests/select-dropdown.test.ts | 152 +++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 packages/forms/tests/select-dropdown.test.ts diff --git a/packages/forms/tests/select-dropdown.test.ts b/packages/forms/tests/select-dropdown.test.ts new file mode 100644 index 00000000..7c055ac5 --- /dev/null +++ b/packages/forms/tests/select-dropdown.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from 'vitest'; +import { createSchema, selectDropdownConfig, type SelectDropdownPattern } from '../src/patterns/select-dropdown'; + +describe('SelectDropdownPattern tests', () => { + describe('createSchema', () => { + it('should create schema for required dropdown', () => { + const data: SelectDropdownPattern['data'] = { + label: 'Test Label', + required: true, + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }; + + const schema = createSchema(data); + expect(schema.safeParse('option1').success).toBe(true); + expect(schema.safeParse('option2').success).toBe(true); + expect(schema.safeParse('invalid').success).toBe(false); + expect(schema.safeParse('').success).toBe(false); + expect(() => schema.parse('')).toThrow(); + }); + + it('should create schema for optional dropdown', () => { + const data: SelectDropdownPattern['data'] = { + label: 'Test Label', + required: false, + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }; + + const schema = createSchema(data); + expect(schema.safeParse('option1').success).toBe(true); + expect(schema.safeParse('option2').success).toBe(true); + expect(schema.safeParse('invalid').success).toBe(false); + expect(schema.safeParse('').success).toBe(true); // Empty string should be valid for optional fields + }); + + it('should throw error if no options are provided', () => { + const data: SelectDropdownPattern['data'] = { + label: 'Test Label', + required: true, + options: [], + }; + + expect(() => createSchema(data)).toThrow('Options must have at least one value'); + }); + }); + + describe('selectDropdownConfig', () => { + it('should parse user input correctly', () => { + const pattern: SelectDropdownPattern = { + type: 'selectDropdown', + id: 'test', + data: { + label: 'Test Dropdown', + required: true, + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }, + }; + + const inputObj = { value: 'option1' }; + if (selectDropdownConfig.parseUserInput) { + const result = selectDropdownConfig.parseUserInput(pattern, inputObj); + console.log('Test parse result:', result); + if (result.success) { + expect(result.data).toBe('option1'); + } else { + throw new Error('Unexpected validation failure'); + } + } else { + throw new Error('parseUserInput is undefined'); + } + }); + + it('should handle validation error for user input', () => { + const pattern: SelectDropdownPattern = { + type: 'selectDropdown', + id: 'test', + data: { + label: 'Test Dropdown', + required: true, + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }, + }; + + const inputObj = { value: 'invalid' }; + if (selectDropdownConfig.parseUserInput) { + const result = selectDropdownConfig.parseUserInput(pattern, inputObj); + console.log('Test parse result (error case):', result); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + throw new Error('Unexpected validation success'); + } + } else { + throw new Error('parseUserInput is undefined'); + } + }); + + it('should parse config data correctly', () => { + const obj = { + label: 'Test Dropdown', + required: true, + options: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }; + + if (selectDropdownConfig.parseConfigData) { + const result = selectDropdownConfig.parseConfigData(obj); + if (result.success) { + expect(result.data.label).toBe('Test Dropdown'); + expect(result.data.required).toBe(true); + expect(result.data.options.length).toBe(2); + } else { + throw new Error('Unexpected validation failure'); + } + } else { + throw new Error('parseConfigData is undefined'); + } + }); + + it('should handle invalid config data', () => { + const obj = { + label: '', + required: true, + options: [], + }; + + if (selectDropdownConfig.parseConfigData) { + const result = selectDropdownConfig.parseConfigData(obj); + if (!result.success) { + expect(result.error).toBeDefined(); + } else { + throw new Error('Unexpected validation success'); + } + } else { + throw new Error('parseConfigData is undefined'); + } + }); + }); +}); \ No newline at end of file From 77cbc96b4c3b2d887a08ac3b848b75c733cda965 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Fri, 1 Nov 2024 08:58:20 -0700 Subject: [PATCH 6/9] refactor: restructure file folder for select-dropdown --- packages/forms/src/patterns/index.ts | 4 ++-- .../select-dropdown}/select-dropdown.test.ts | 5 ++-- .../{ => select-dropdown}/select-dropdown.ts | 24 +++++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) rename packages/forms/{tests => src/patterns/select-dropdown}/select-dropdown.test.ts (95%) rename packages/forms/src/patterns/{ => select-dropdown}/select-dropdown.ts (79%) diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index 11efe36b..c6c1d006 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -11,7 +11,7 @@ import { pageSetConfig } from './page-set/index.js'; import { paragraphConfig } from './paragraph.js'; import { radioGroupConfig } from './radio-group.js'; import { richTextConfig } from './rich-text.js'; -import { selectDropdownConfig } from './select-dropdown.js'; +import { selectDropdownConfig } from './select-dropdown/select-dropdown.js'; import { sequenceConfig } from './sequence.js'; // This configuration reflects what a user of this library would provide for @@ -49,5 +49,5 @@ export * from './page-set/index.js'; export { type PageSetPattern } from './page-set/config.js'; export * from './paragraph.js'; export * from './radio-group.js'; -export * from './select-dropdown.js'; +export * from './select-dropdown/select-dropdown.js'; export * from './sequence.js'; diff --git a/packages/forms/tests/select-dropdown.test.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts similarity index 95% rename from packages/forms/tests/select-dropdown.test.ts rename to packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts index 7c055ac5..d317a72d 100644 --- a/packages/forms/tests/select-dropdown.test.ts +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createSchema, selectDropdownConfig, type SelectDropdownPattern } from '../src/patterns/select-dropdown'; +import { createSchema, selectDropdownConfig, type SelectDropdownPattern } from './select-dropdown'; describe('SelectDropdownPattern tests', () => { describe('createSchema', () => { @@ -35,7 +35,7 @@ describe('SelectDropdownPattern tests', () => { expect(schema.safeParse('option1').success).toBe(true); expect(schema.safeParse('option2').success).toBe(true); expect(schema.safeParse('invalid').success).toBe(false); - expect(schema.safeParse('').success).toBe(true); // Empty string should be valid for optional fields + expect(schema.safeParse('').success).toBe(true); }); it('should throw error if no options are provided', () => { @@ -98,6 +98,7 @@ describe('SelectDropdownPattern tests', () => { console.log('Test parse result (error case):', result); if (!result.success) { expect(result.error).toBeDefined(); + expect(result.error.message).toBe("Invalid enum value. Expected 'option1' | 'option2', received 'invalid'"); } else { throw new Error('Unexpected validation success'); } diff --git a/packages/forms/src/patterns/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts similarity index 79% rename from packages/forms/src/patterns/select-dropdown.ts rename to packages/forms/src/patterns/select-dropdown/select-dropdown.ts index 8c7e2216..6fc8e4db 100644 --- a/packages/forms/src/patterns/select-dropdown.ts +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts @@ -1,13 +1,13 @@ import * as z from 'zod'; -import { type SelectDropdownProps } from '../components.js'; +import { type SelectDropdownProps } from '../../components.js'; import { type Pattern, type PatternConfig, validatePattern, -} from '../pattern.js'; -import { getFormSessionValue } from '../session.js'; -import { safeZodParseFormErrors } from '../util/zod.js'; +} from '../../pattern.js'; +import { getFormSessionValue } from '../../session.js'; +import { safeZodParseFormErrors, safeZodParseToFormError } from '../../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), @@ -88,6 +88,22 @@ export const selectDropdownConfig: PatternConfig< } }, + // parseUserInput: (pattern, inputObj) => { + // const expectedInput = inputObj as { value: string }; + // console.log('TEST parseUserInput', pattern, expectedInput); + + // const schema = createSchema(pattern.data); + // console.log('TEST schema', schema); + // const result = schema.parse(expectedInput.value); + // console.log('TEST result', result); + + // if (result.success) { + // return { success: true, data: result.data }; + // } else { + // return { success: false, error: safeZodParseToFormError(schema, obj) }; + // } + // }, + parseConfigData: obj => { const result = safeZodParseFormErrors(configSchema, obj); return result; From 56ca56cc11f4ee464cf4d113046668f622db9608 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Fri, 1 Nov 2024 10:36:39 -0700 Subject: [PATCH 7/9] feat: refactor parseUserInput and update tests --- .../select-dropdown/select-dropdown.test.ts | 54 +++++++++++-------- .../select-dropdown/select-dropdown.ts | 50 +++-------------- 2 files changed, 37 insertions(+), 67 deletions(-) diff --git a/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts index d317a72d..2149a091 100644 --- a/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { createSchema, selectDropdownConfig, type SelectDropdownPattern } from './select-dropdown'; +import { + createSchema, + selectDropdownConfig, + type SelectDropdownPattern, +} from './select-dropdown'; describe('SelectDropdownPattern tests', () => { describe('createSchema', () => { @@ -8,14 +12,14 @@ describe('SelectDropdownPattern tests', () => { label: 'Test Label', required: true, options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, ], }; const schema = createSchema(data); - expect(schema.safeParse('option1').success).toBe(true); - expect(schema.safeParse('option2').success).toBe(true); + expect(schema.safeParse('value1').success).toBe(true); + expect(schema.safeParse('value2').success).toBe(true); expect(schema.safeParse('invalid').success).toBe(false); expect(schema.safeParse('').success).toBe(false); expect(() => schema.parse('')).toThrow(); @@ -26,14 +30,14 @@ describe('SelectDropdownPattern tests', () => { label: 'Test Label', required: false, options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, ], }; const schema = createSchema(data); - expect(schema.safeParse('option1').success).toBe(true); - expect(schema.safeParse('option2').success).toBe(true); + expect(schema.safeParse('value1').success).toBe(true); + expect(schema.safeParse('value2').success).toBe(true); expect(schema.safeParse('invalid').success).toBe(false); expect(schema.safeParse('').success).toBe(true); }); @@ -45,7 +49,9 @@ describe('SelectDropdownPattern tests', () => { options: [], }; - expect(() => createSchema(data)).toThrow('Options must have at least one value'); + expect(() => createSchema(data)).toThrow( + 'Options must have at least one value' + ); }); }); @@ -58,18 +64,18 @@ describe('SelectDropdownPattern tests', () => { label: 'Test Dropdown', required: true, options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, ], }, }; - const inputObj = { value: 'option1' }; + const inputValue = 'value1'; if (selectDropdownConfig.parseUserInput) { - const result = selectDropdownConfig.parseUserInput(pattern, inputObj); + const result = selectDropdownConfig.parseUserInput(pattern, inputValue); console.log('Test parse result:', result); if (result.success) { - expect(result.data).toBe('option1'); + expect(result.data).toBe('value1'); } else { throw new Error('Unexpected validation failure'); } @@ -86,19 +92,21 @@ describe('SelectDropdownPattern tests', () => { label: 'Test Dropdown', required: true, options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, ], }, }; - const inputObj = { value: 'invalid' }; + const inputValue = 'invalid'; if (selectDropdownConfig.parseUserInput) { - const result = selectDropdownConfig.parseUserInput(pattern, inputObj); + const result = selectDropdownConfig.parseUserInput(pattern, inputValue); console.log('Test parse result (error case):', result); if (!result.success) { expect(result.error).toBeDefined(); - expect(result.error.message).toBe("Invalid enum value. Expected 'option1' | 'option2', received 'invalid'"); + expect(result.error.message).toBe( + "Invalid enum value. Expected 'value1' | 'value2', received 'invalid'" + ); } else { throw new Error('Unexpected validation success'); } @@ -112,8 +120,8 @@ describe('SelectDropdownPattern tests', () => { label: 'Test Dropdown', required: true, options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, ], }; @@ -150,4 +158,4 @@ describe('SelectDropdownPattern tests', () => { } }); }); -}); \ No newline at end of file +}); diff --git a/packages/forms/src/patterns/select-dropdown/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts index 6fc8e4db..5e0fa8e6 100644 --- a/packages/forms/src/patterns/select-dropdown/select-dropdown.ts +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts @@ -7,7 +7,10 @@ import { validatePattern, } from '../../pattern.js'; import { getFormSessionValue } from '../../session.js'; -import { safeZodParseFormErrors, safeZodParseToFormError } from '../../util/zod.js'; +import { + safeZodParseFormErrors, + safeZodParseToFormError, +} from '../../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), @@ -59,51 +62,10 @@ export const selectDropdownConfig: PatternConfig< ], }, - parseUserInput: (pattern, inputObj) => { - const expectedInput = inputObj as { value: string }; - - const schema = createSchema(pattern.data); - try { - const parsedValue = schema.parse(expectedInput.value); - return parsedValue - ? { success: true, data: parsedValue } - : { - success: false, - error: { - type: 'custom', - message: 'Parsed select dropdown value is undefined', - }, - }; - } catch (e) { - const zodError = e as z.ZodError; - return { - success: false, - error: { - type: 'custom', - message: zodError.errors - ? zodError.errors[0].message - : zodError.message, - }, - }; - } + parseUserInput: (pattern, inputValue) => { + return safeZodParseToFormError(createSchema(pattern['data']), inputValue); }, - // parseUserInput: (pattern, inputObj) => { - // const expectedInput = inputObj as { value: string }; - // console.log('TEST parseUserInput', pattern, expectedInput); - - // const schema = createSchema(pattern.data); - // console.log('TEST schema', schema); - // const result = schema.parse(expectedInput.value); - // console.log('TEST result', result); - - // if (result.success) { - // return { success: true, data: result.data }; - // } else { - // return { success: false, error: safeZodParseToFormError(schema, obj) }; - // } - // }, - parseConfigData: obj => { const result = safeZodParseFormErrors(configSchema, obj); return result; From 3b06384beec006f31e2b6f3c6fafd8e75912cf42 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Fri, 1 Nov 2024 12:06:32 -0700 Subject: [PATCH 8/9] feat: remoce pdf proccessing code for select dropdown tckt-360 --- packages/forms/src/documents/document.ts | 11 ----------- packages/forms/src/documents/pdf/generate.ts | 3 --- packages/forms/src/documents/pdf/index.ts | 3 +-- packages/forms/src/documents/types.ts | 10 ---------- 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index 30f9507f..2e455702 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -137,17 +137,6 @@ export const addDocumentFieldsToForm = ( maxLength: 128, }, } satisfies InputPattern); - } else if (field.type === 'SelectDropdown') { - patterns.push({ - type: 'input', - id: patternId, - data: { - label: field.label, - initial: '', - required: false, - maxLength: 128, - }, - } satisfies InputPattern); } else if (field.type === 'Paragraph' || field.type === 'RichText') { // skip purely presentational fields } else if (field.type === 'not-supported') { diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index 50c98c17..0750d87c 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -113,9 +113,6 @@ const setFormFieldData = ( } else if (fieldType === 'OptionList') { const field = form.getDropdown(fieldName); field.select(fieldValue); - } else if (fieldType === 'SelectDropdown') { - const field = form.getDropdown(fieldName); - field.select(fieldValue); } else if (fieldType === 'RadioGroup') { // TODO: remove this when we have a better way to handle radio groups try { diff --git a/packages/forms/src/documents/pdf/index.ts b/packages/forms/src/documents/pdf/index.ts index 9a74b5b1..6cdd0aba 100644 --- a/packages/forms/src/documents/pdf/index.ts +++ b/packages/forms/src/documents/pdf/index.ts @@ -19,5 +19,4 @@ export type PDFFieldType = | 'OptionList' | 'RadioGroup' | 'Paragraph' - | 'RichText' - | 'SelectDropdown'; + | 'RichText'; diff --git a/packages/forms/src/documents/types.ts b/packages/forms/src/documents/types.ts index fad1ad0f..a94d4d2a 100644 --- a/packages/forms/src/documents/types.ts +++ b/packages/forms/src/documents/types.ts @@ -36,16 +36,6 @@ export type DocumentFieldValue = value: string; required: boolean; } - | { - type: 'SelectDropdown'; - name: string; - options: { - value: string; - label: string; - }[]; - label: string; - required: boolean; - } | { type: 'Paragraph'; name: string; From 8abccf7a4b68777cc7b44b37310a3b0001703c12 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Fri, 1 Nov 2024 12:47:50 -0700 Subject: [PATCH 9/9] test: refactor tests tckt-360 --- .../forms/src/documents/pdf/parsing-api.ts | 9 +-- .../select-dropdown/select-dropdown.test.ts | 74 +++++++++---------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index e77749b3..82d44f15 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -83,14 +83,7 @@ const ExtractedObject = z.object({ raw_text: z.string(), form_summary: FormSummary, elements: z - .union([ - TxInput, - Checkbox, - RadioGroup, - Paragraph, - Fieldset, - RichText, - ]) + .union([TxInput, Checkbox, RadioGroup, Paragraph, Fieldset, RichText]) .array(), }); diff --git a/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts index 2149a091..3d728330 100644 --- a/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts @@ -71,16 +71,15 @@ describe('SelectDropdownPattern tests', () => { }; const inputValue = 'value1'; - if (selectDropdownConfig.parseUserInput) { - const result = selectDropdownConfig.parseUserInput(pattern, inputValue); - console.log('Test parse result:', result); - if (result.success) { - expect(result.data).toBe('value1'); - } else { - throw new Error('Unexpected validation failure'); - } + if (!selectDropdownConfig.parseUserInput) { + expect.fail('selectDropdownConfig.parseUserInput is not undefined'); + } + const result = selectDropdownConfig.parseUserInput(pattern, inputValue); + console.log('Test parse result:', result); + if (result.success) { + expect(result.data).toBe('value1'); } else { - throw new Error('parseUserInput is undefined'); + throw new Error('Unexpected validation failure'); } }); @@ -99,19 +98,18 @@ describe('SelectDropdownPattern tests', () => { }; const inputValue = 'invalid'; - if (selectDropdownConfig.parseUserInput) { - const result = selectDropdownConfig.parseUserInput(pattern, inputValue); - console.log('Test parse result (error case):', result); - if (!result.success) { - expect(result.error).toBeDefined(); - expect(result.error.message).toBe( - "Invalid enum value. Expected 'value1' | 'value2', received 'invalid'" - ); - } else { - throw new Error('Unexpected validation success'); - } + if (!selectDropdownConfig.parseUserInput) { + expect.fail('selectDropdownConfig.parseUserInput is not undefined'); + } + const result = selectDropdownConfig.parseUserInput(pattern, inputValue); + console.log('Test parse result (error case):', result); + if (!result.success) { + expect(result.error).toBeDefined(); + expect(result.error.message).toBe( + "Invalid enum value. Expected 'value1' | 'value2', received 'invalid'" + ); } else { - throw new Error('parseUserInput is undefined'); + throw new Error('Unexpected validation success'); } }); @@ -125,17 +123,16 @@ describe('SelectDropdownPattern tests', () => { ], }; - if (selectDropdownConfig.parseConfigData) { - const result = selectDropdownConfig.parseConfigData(obj); - if (result.success) { - expect(result.data.label).toBe('Test Dropdown'); - expect(result.data.required).toBe(true); - expect(result.data.options.length).toBe(2); - } else { - throw new Error('Unexpected validation failure'); - } + if (!selectDropdownConfig.parseConfigData) { + expect.fail('selectDropdownConfig.parseConfigData is not undefined'); + } + const result = selectDropdownConfig.parseConfigData(obj); + if (result.success) { + expect(result.data.label).toBe('Test Dropdown'); + expect(result.data.required).toBe(true); + expect(result.data.options.length).toBe(2); } else { - throw new Error('parseConfigData is undefined'); + throw new Error('Unexpected validation failure'); } }); @@ -146,15 +143,14 @@ describe('SelectDropdownPattern tests', () => { options: [], }; - if (selectDropdownConfig.parseConfigData) { - const result = selectDropdownConfig.parseConfigData(obj); - if (!result.success) { - expect(result.error).toBeDefined(); - } else { - throw new Error('Unexpected validation success'); - } + if (!selectDropdownConfig.parseConfigData) { + expect.fail('selectDropdownConfig.parseConfigData is not undefined'); + } + const result = selectDropdownConfig.parseConfigData(obj); + if (!result.success) { + expect(result.error).toBeDefined(); } else { - throw new Error('parseConfigData is undefined'); + throw new Error('Unexpected validation success'); } }); });