From 8a7f9827a9a861a7ad510878dbd7faaffbf1b401 Mon Sep 17 00:00:00 2001 From: Khayal Alasgarov Date: Fri, 1 Nov 2024 12:59:03 -0700 Subject: [PATCH] feat: create select dropdown patterns for form builders and form users tckt-360 (#370) * feat: create select dropdown pattern tckt-360 * feat: create select dropdown pattern edit for form builder tckt-360 * feat: refactor rendering default select option via the template tckt-360 * fix: parseUserInput hook for select dropdown tckt-360 * test: add unit tests for select dropdown pattern tckt-360 * refactor: restructure file folder for select-dropdown * feat: refactor parseUserInput and update tests * feat: remoce pdf proccessing code for select dropdown tckt-360 * test: refactor tests tckt-360 --------- Co-authored-by: Khayal Alasgarov --- packages/common/src/locales/en/app.ts | 6 + .../SelectDropdown/SelectDropdown.stories.tsx | 74 +++++++++ .../SelectDropdown/SelectDropdown.test.tsx | 7 + .../SelectDropdown/SelectDropdown.tsx | 40 +++++ .../Form/components/SelectDropdown/index.tsx | 3 + packages/design/src/Form/components/index.tsx | 2 + .../FormEdit/AddPatternDropdown.tsx | 2 + .../SelectDropdownPatternEdit.stories.tsx | 129 ++++++++++++++ .../SelectDropdownPatternEdit.test.tsx | 7 + .../components/SelectDropdownPatternEdit.tsx | 144 ++++++++++++++++ .../FormManager/FormEdit/components/index.ts | 2 + packages/forms/src/components.ts | 12 ++ packages/forms/src/patterns/index.ts | 3 + .../select-dropdown/select-dropdown.test.ts | 157 ++++++++++++++++++ .../select-dropdown/select-dropdown.ts | 108 ++++++++++++ 15 files changed, 696 insertions(+) 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/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 create mode 100644 packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts create mode 100644 packages/forms/src/patterns/select-dropdown/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..51fe214c --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.stories.tsx @@ -0,0 +1,74 @@ +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: '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: '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: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', 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..188e3bd8 --- /dev/null +++ b/packages/design/src/Form/components/SelectDropdown/SelectDropdown.tsx @@ -0,0 +1,40 @@ +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/SelectDropdownPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx new file mode 100644 index 00000000..564864e4 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/SelectDropdownPatternEdit.stories.tsx @@ -0,0 +1,129 @@ +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: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, + ], + }, +}; + +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 3 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; 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/patterns/index.ts b/packages/forms/src/patterns/index.ts index f502ffc4..c6c1d006 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/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/select-dropdown.js'; export * from './sequence.js'; diff --git a/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts new file mode 100644 index 00000000..3d728330 --- /dev/null +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest'; +import { + createSchema, + selectDropdownConfig, + type SelectDropdownPattern, +} from './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: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }; + + const schema = createSchema(data); + 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(); + }); + + it('should create schema for optional dropdown', () => { + const data: SelectDropdownPattern['data'] = { + label: 'Test Label', + required: false, + options: [ + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }; + + const schema = createSchema(data); + 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); + }); + + 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: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }, + }; + + const inputValue = 'value1'; + 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('Unexpected validation failure'); + } + }); + + it('should handle validation error for user input', () => { + const pattern: SelectDropdownPattern = { + type: 'selectDropdown', + id: 'test', + data: { + label: 'Test Dropdown', + required: true, + options: [ + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }, + }; + + const inputValue = 'invalid'; + 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('Unexpected validation success'); + } + }); + + it('should parse config data correctly', () => { + const obj = { + label: 'Test Dropdown', + required: true, + options: [ + { value: 'value1', label: 'Option 1' }, + { value: 'value2', label: 'Option 2' }, + ], + }; + + 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('Unexpected validation failure'); + } + }); + + it('should handle invalid config data', () => { + const obj = { + label: '', + required: true, + options: [], + }; + + 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('Unexpected validation success'); + } + }); + }); +}); diff --git a/packages/forms/src/patterns/select-dropdown/select-dropdown.ts b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts new file mode 100644 index 00000000..5e0fa8e6 --- /dev/null +++ b/packages/forms/src/patterns/select-dropdown/select-dropdown.ts @@ -0,0 +1,108 @@ +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, + safeZodParseToFormError, +} 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>; + +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, + SelectDropdownPatternOutput +> = { + displayName: 'Select Dropdown', + iconPath: 'dropdown-icon.svg', + initial: { + label: 'Select-dropdown-label', + required: true, + options: [ + { value: 'value1', label: 'Option-1' }, + { value: 'value2', label: 'Option-2' }, + { value: 'value3', label: 'Option-3' }, + ], + }, + + parseUserInput: (pattern, inputValue) => { + return safeZodParseToFormError(createSchema(pattern['data']), inputValue); + }, + + parseConfigData: obj => { + const result = safeZodParseFormErrors(configSchema, obj); + return result; + }, + getChildren() { + return []; + }, + + 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: [], + }; + }, +};