From 68a1ee4e203179a91ab0ab0e579f95c60fe77694 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Wed, 11 Sep 2024 13:42:27 -0400 Subject: [PATCH 01/27] copy fieldset pattern to use as the basis for the repeater pattern (multiple input) --- packages/common/src/locales/en/app.ts | 5 + .../components/Repeater/Repeater.stories.tsx | 17 ++ .../components/Repeater/Repeater.test.tsx | 7 + .../src/Form/components/Repeater/index.tsx | 20 +++ packages/design/src/Form/components/index.tsx | 2 + .../FormEdit/AddPatternDropdown.tsx | 1 + .../RepeaterPatternEdit.stories.tsx | 108 +++++++++++++ .../components/RepeaterPatternEdit.test.tsx | 7 + .../components/RepeaterPatternEdit.tsx | 152 ++++++++++++++++++ .../FormManager/FormEdit/components/index.ts | 2 + packages/forms/src/components.ts | 7 + packages/forms/src/documents/document.ts | 6 +- packages/forms/src/documents/pdf/generate.ts | 6 +- packages/forms/src/documents/pdf/index.ts | 1 + .../forms/src/documents/pdf/parsing-api.ts | 86 +++++++++- packages/forms/src/documents/types.ts | 8 + packages/forms/src/index.ts | 1 + packages/forms/src/patterns/index.ts | 3 + .../forms/src/patterns/repeater/config.ts | 31 ++++ packages/forms/src/patterns/repeater/index.ts | 43 +++++ .../forms/src/patterns/repeater/prompt.ts | 27 ++++ 21 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 packages/design/src/Form/components/Repeater/Repeater.stories.tsx create mode 100644 packages/design/src/Form/components/Repeater/Repeater.test.tsx create mode 100644 packages/design/src/Form/components/Repeater/index.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.stories.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.test.tsx create mode 100644 packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx create mode 100644 packages/forms/src/patterns/repeater/config.ts create mode 100644 packages/forms/src/patterns/repeater/index.ts create mode 100644 packages/forms/src/patterns/repeater/prompt.ts diff --git a/packages/common/src/locales/en/app.ts b/packages/common/src/locales/en/app.ts index d8b17262..052a96c7 100644 --- a/packages/common/src/locales/en/app.ts +++ b/packages/common/src/locales/en/app.ts @@ -41,5 +41,10 @@ export const en = { fieldLabel: 'Radio group label', errorTextMustContainChar: 'String must contain at least 1 character(s)', }, + repeater: { + ...defaults, + displayName: 'Repeatable Group', + errorTextMustContainChar: 'String must contain at least 1 character(s)', + }, }, }; diff --git a/packages/design/src/Form/components/Repeater/Repeater.stories.tsx b/packages/design/src/Form/components/Repeater/Repeater.stories.tsx new file mode 100644 index 00000000..5e9931c0 --- /dev/null +++ b/packages/design/src/Form/components/Repeater/Repeater.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Repeater from './index.js'; + +export default { + title: 'patterns/Repeater', + component: Repeater, + tags: ['autodocs'], +} satisfies Meta; + +export const RepeaterSection = { + args: { + legend: 'Default Heading', + type: 'repeater', + _patternId: 'test-id', + }, +} satisfies StoryObj; diff --git a/packages/design/src/Form/components/Repeater/Repeater.test.tsx b/packages/design/src/Form/components/Repeater/Repeater.test.tsx new file mode 100644 index 00000000..745c5a9e --- /dev/null +++ b/packages/design/src/Form/components/Repeater/Repeater.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './Repeater.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx new file mode 100644 index 00000000..b918e3f0 --- /dev/null +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { type RepeaterProps } from '@atj/forms'; + +import { type PatternComponent } from '../../../Form/index.js'; + +const Repeater: PatternComponent = props => { + return ( +
+ {props.legend !== '' && props.legend !== undefined && ( + + {props.legend} + + )} + + {props.children} +
+ ); +}; +export default Repeater; diff --git a/packages/design/src/Form/components/index.tsx b/packages/design/src/Form/components/index.tsx index ede184b1..2ccf11fe 100644 --- a/packages/design/src/Form/components/index.tsx +++ b/packages/design/src/Form/components/index.tsx @@ -8,6 +8,7 @@ import Page from './Page/index.js'; import PageSet from './PageSet/index.js'; import Paragraph from './Paragraph/index.js'; import RadioGroup from './RadioGroup/index.js'; +import Repeater from './Repeater/index.js'; import RichText from './RichText/index.js'; import Sequence from './Sequence/index.js'; import SubmissionConfirmation from './SubmissionConfirmation/index.js'; @@ -23,6 +24,7 @@ export const defaultPatternComponents: ComponentForPattern = { 'page-set': PageSet as PatternComponent, paragraph: Paragraph as PatternComponent, 'radio-group': RadioGroup as PatternComponent, + repeater: Repeater as PatternComponent, 'rich-text': RichText 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 822619df..f7b03f2d 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -92,6 +92,7 @@ const sidebarPatterns: DropdownPattern[] = [ ['input', defaultFormConfig.patterns['input']], ['paragraph', defaultFormConfig.patterns['paragraph']], ['rich-text', defaultFormConfig.patterns['rich-text']], + ['repeater', defaultFormConfig.patterns['repeater']], ['radio-group', defaultFormConfig.patterns['radio-group']], ] as const; export const fieldsetPatterns: DropdownPattern[] = [ diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.stories.tsx new file mode 100644 index 00000000..ccc65d83 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent } from '@storybook/test'; +import { within } from '@testing-library/react'; + +import { enLocale as message } from '@atj/common'; +import { type RepeaterPattern } from '@atj/forms'; + +import { + createPatternEditStoryMeta, + testEmptyFormLabelErrorByElement, + testUpdateFormFieldOnSubmitByElement, +} from './common/story-helper.js'; +import FormEdit from '../index.js'; + +const pattern: RepeaterPattern = { + id: '1', + type: 'repeater', + data: { + legend: 'Repeater pattern description', + patterns: [], + }, +}; + +const storyConfig: Meta = { + title: 'Edit components/RepeaterPattern', + ...createPatternEditStoryMeta({ + pattern, + }), +} as Meta; +export default storyConfig; + +export const Basic: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await testUpdateFormFieldOnSubmitByElement( + canvasElement, + await canvas.findByText('Repeater pattern description'), + 'Legend Text Element', + 'Updated repeater pattern' + ); + }, +}; + +export const Error: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await testEmptyFormLabelErrorByElement( + canvasElement, + await canvas.findByText('Repeater pattern description'), + 'Legend Text Element', + message.patterns.repeater.errorTextMustContainChar + ); + }, +}; + +export const AddPattern: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Add a "short answer" question + const addQuestionButton = canvas.getByRole('button', { + name: /Add question/, + }); + await userEvent.click(addQuestionButton); + const shortAnswerButton = canvas.getByRole('button', { + name: /Short answer/, + }); + await userEvent.click(shortAnswerButton); + + // Submit new field's edit form + const input = await canvas.findByLabelText('Field label'); + await userEvent.clear(input); + await userEvent.type(input, 'Repeater short question'); + const form = input?.closest('form'); + form?.requestSubmit(); + + // Confirm that the "short answer" field exists + const updatedElement = await canvas.findAllByText( + 'Repeater short question' + ); + await expect(updatedElement.length).toBeGreaterThan(0); + }, +}; + +export const RemovePattern: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Confirm that the expected repeater legend exists + expect( + canvas.queryAllByRole('group', { + name: /Repeater pattern description/i, + }) + ).toHaveLength(1); + + // Add a "short answer" question + const removeSectionButton = canvas.getByRole('button', { + name: /Remove section/, + }); + await userEvent.click(removeSectionButton); + + // Confirm that the repeater was removed + const test = await canvas.queryAllByRole('group', { + name: /Repeater pattern description/i, + }); + expect(test).toHaveLength(0); + }, +}; diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.test.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.test.tsx new file mode 100644 index 00000000..63f795a4 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.test.tsx @@ -0,0 +1,7 @@ +/** + * @vitest-environment jsdom + */ +import { describeStories } from '../../../test-helper.js'; +import meta, * as stories from './RepeaterPatternEdit.stories.js'; + +describeStories(meta, stories); diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx new file mode 100644 index 00000000..de509974 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -0,0 +1,152 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { type PatternId, type RepeaterProps } from '@atj/forms'; +import { RepeaterPattern } from '@atj/forms'; + +// import { +// RepeaterAddPatternButton, +// RepeaterEmptyStateAddPatternButton, +// } from '../AddPatternDropdown.js'; +import { PatternComponent } from '../../../Form/index.js'; +import Repeater from '../../../Form/components/Repeater/index.js'; +import { useFormManagerStore } from '../../store.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 styles from '../formEditStyles.module.css'; + +const RepeaterEdit: PatternEditComponent = ({ + focus, + previewProps, +}) => { + return ( + <> + {focus ? ( +

Edit pattern

+ ) : ( + // } + // > +

Preview pattern

+ // + )} + + ); +}; + +const RepeaterPreview: PatternComponent = props => { + const { addPatternToRepeater, deletePattern } = useFormManagerStore( + state => ({ + addPatternToRepeater: state.addPatternToRepeater, + deletePattern: state.deletePattern, + }) + ); + const pattern = useFormManagerStore( + state => state.session.form.patterns[props._patternId] + ); + return ( + <> + + {props.children} + {pattern && pattern.data.patterns.length === 0 && ( +
+
+
+ + Empty sections will not display. + + + + addPatternToRepeater(patternType, props._patternId) + } + /> + + + + +
+
+
+ )} + {pattern.data.patterns.length > 0 && ( +
+
+ + addPatternToRepeater(patternType, props._patternId) + } + /> +
+
+ )} +
+ + ); +}; + +const EditComponent = ({ patternId }: { patternId: PatternId }) => { + const pattern = useFormManagerStore( + state => state.session.form.patterns[patternId] + ); + const { fieldId, getFieldState, register } = + usePatternEditFormContext(patternId); + const legend = getFieldState('legend'); + return ( +
+
+ + + +
+ + +
+ ); +}; + +export default RepeaterEdit; diff --git a/packages/design/src/FormManager/FormEdit/components/index.ts b/packages/design/src/FormManager/FormEdit/components/index.ts index 2db6176c..038bd583 100644 --- a/packages/design/src/FormManager/FormEdit/components/index.ts +++ b/packages/design/src/FormManager/FormEdit/components/index.ts @@ -12,6 +12,7 @@ import PageSetEdit from './PageSetEdit.js'; import ParagraphPatternEdit from './ParagraphPatternEdit.js'; import { PatternPreviewSequence } from './PreviewSequencePattern/index.js'; import RadioGroupPatternEdit from './RadioGroupPatternEdit.js'; +import RepeaterPatternEdit from './RepeaterPatternEdit.js'; import RichTextPatternEdit from './RichTextPatternEdit/index.js'; import SubmissionConfirmationEdit from './SubmissionConfirmationEdit.js'; @@ -24,6 +25,7 @@ export const defaultPatternEditComponents: EditComponentForPattern = { page: PageEdit as PatternEditComponent, 'page-set': PageSetEdit as PatternEditComponent, 'radio-group': RadioGroupPatternEdit as PatternEditComponent, + repeater: RepeaterPatternEdit as PatternEditComponent, 'rich-text': RichTextPatternEdit 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 e98cf1e2..68c0a27d 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -81,6 +81,13 @@ export type RadioGroupProps = PatternProps<{ }[]; }>; +export type RepeaterProps = PatternProps<{ + type: 'repeater'; + legend?: string; + subHeading?: string; + 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 4aba8c8f..d8648024 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -135,7 +135,11 @@ export const addDocumentFieldsToForm = ( maxLength: 128, }, } satisfies InputPattern); - } else if (field.type === 'Paragraph' || field.type === 'RichText') { + } else if ( + field.type === 'Paragraph' || + field.type === 'RichText' || + field.type === 'Repeater' + ) { // skip purely presentational fields } else if (field.type === 'not-supported') { console.error(`Skipping field: ${field.error}`); diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index 0750d87c..48403883 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -129,7 +129,11 @@ const setFormFieldData = ( field.uncheck(); } } - } else if (fieldType === 'Paragraph' || fieldType === 'RichText') { + } else if ( + fieldType === 'Paragraph' || + fieldType === 'RichText' || + fieldType === 'Repeater' + ) { // do nothing } else { const exhaustiveCheck: never = fieldType; diff --git a/packages/forms/src/documents/pdf/index.ts b/packages/forms/src/documents/pdf/index.ts index 6cdd0aba..68f1a922 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' + | 'Repeater' | 'RichText'; diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index 1ca8a01c..00e9d352 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -15,6 +15,7 @@ import { type InputPattern } from '../../patterns/input/index.js'; import { type ParagraphPattern } from '../../patterns/paragraph.js'; import { type CheckboxPattern } from '../../patterns/checkbox.js'; import { type RadioGroupPattern } from '../../patterns/radio-group.js'; +import { type RepeaterPattern } from '../../patterns/repeater/index.js'; import { uint8ArrayToBase64 } from '../util.js'; import { type DocumentFieldMap } from '../types.js'; @@ -80,11 +81,26 @@ const Fieldset = z.object({ page: z.union([z.number(), z.string()]), }); +const Repeater = z.object({ + component_type: z.literal('repeater'), + legend: z.string(), + fields: z.union([TxInput, Checkbox]).array(), + page: z.union([z.number(), z.string()]), +}); + 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, + Repeater, + ]) .array(), }); @@ -324,6 +340,74 @@ export const processApiResponse = async (json: any): Promise => { ); } } + + if (element.component_type === 'repeater') { + for (const input of element.fields) { + if (input.component_type === 'text_input') { + const inputPattern = processPatternData( + defaultFormConfig, + parsedPdf, + 'input', + { + label: input.label, + required: false, + initial: '', + maxLength: 128, + } + ); + if (inputPattern) { + fieldsetPatterns.push(inputPattern.id); + parsedPdf.outputs[inputPattern.id] = { + type: 'TextField', + name: input.id, + label: input.label, + value: '', + maxLength: 1024, + required: input.required, + }; + } + } + if (input.component_type === 'checkbox') { + const checkboxPattern = processPatternData( + defaultFormConfig, + parsedPdf, + 'checkbox', + { + label: input.label, + defaultChecked: false, + } + ); + if (checkboxPattern) { + fieldsetPatterns.push(checkboxPattern.id); + parsedPdf.outputs[checkboxPattern.id] = { + type: 'CheckBox', + name: input.id, + label: input.label, + value: false, + required: true, + }; + } + } + } + } + + // Add fieldset to parsedPdf.patterns and rootSequence + if (element.component_type === 'repeater' && fieldsetPatterns.length > 0) { + const fieldset = processPatternData( + defaultFormConfig, + parsedPdf, + 'repeater', + { + legend: element.legend, + patterns: fieldsetPatterns, + } + ); + if (fieldset) { + pagePatterns[element.page] = (pagePatterns[element.page] || []).concat( + fieldset.id + ); + } + } } // Create a pattern for the single, first page. diff --git a/packages/forms/src/documents/types.ts b/packages/forms/src/documents/types.ts index a94d4d2a..329f7f72 100644 --- a/packages/forms/src/documents/types.ts +++ b/packages/forms/src/documents/types.ts @@ -44,6 +44,14 @@ export type DocumentFieldValue = value: string; required: boolean; } + | { + type: 'Repeater'; + name: string; + options: string[]; + label: string; + value: string; + required: boolean; + } | { type: 'RichText'; name: string; diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 64d68be0..abcfcb9b 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -24,6 +24,7 @@ import { type PageSetPattern } from './patterns/page-set/config.js'; export { type RichTextPattern } from './patterns/rich-text.js'; import { type SequencePattern } from './patterns/sequence.js'; import { FieldsetPattern } from './patterns/index.js'; +import { RepeaterPattern } from './patterns/index.js'; export { type FormRepository, createFormsRepository, diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index b871634a..aadca313 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 { fieldsetConfig } from './fieldset/index.js'; +import { repeaterConfig } from './repeater/index.js'; import { formSummaryConfig } from './form-summary.js'; import { inputConfig } from './input/index.js'; import { pageConfig } from './page/index.js'; @@ -25,6 +26,7 @@ export const defaultFormConfig: FormConfig = { page: pageConfig, 'page-set': pageSetConfig, paragraph: paragraphConfig, + repeater: repeaterConfig, 'rich-text': richTextConfig, 'radio-group': radioGroupConfig, sequence: sequenceConfig, @@ -42,4 +44,5 @@ export * from './checkbox.js'; export * from './form-summary.js'; export * from './paragraph.js'; export * from './radio-group.js'; +export * from './repeater/index.js'; export * from './sequence.js'; diff --git a/packages/forms/src/patterns/repeater/config.ts b/packages/forms/src/patterns/repeater/config.ts new file mode 100644 index 00000000..09633317 --- /dev/null +++ b/packages/forms/src/patterns/repeater/config.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { safeZodParseFormErrors } from '../../util/zod.js'; +import { ParsePatternConfigData } from '../../pattern.js'; + +const configSchema = z.object({ + legend: z.string().min(1), + patterns: z.union([ + // Support either an array of strings... + z.array(z.string()), + // ...or a comma-separated string. + // REVISIT: This is messy, and exists only so we can store the data easily + // as a hidden input in the form. We should probably just store it as JSON. + z + .string() + .transform(value => + value + .split(',') + .map(String) + .filter(value => value) + ) + .pipe(z.string().array()), + ]), +}); +export type RepeaterConfigSchema = z.infer; + +export const parseConfigData: ParsePatternConfigData< + RepeaterConfigSchema +> = obj => { + return safeZodParseFormErrors(configSchema, obj); +}; diff --git a/packages/forms/src/patterns/repeater/index.ts b/packages/forms/src/patterns/repeater/index.ts new file mode 100644 index 00000000..e197a62f --- /dev/null +++ b/packages/forms/src/patterns/repeater/index.ts @@ -0,0 +1,43 @@ +import { + type Pattern, + type PatternConfig, + type PatternId, +} from '../../pattern.js'; +import { parseConfigData } from './config.js'; +import { createPrompt } from './prompt.js'; + +export type RepeaterPattern = Pattern<{ + legend?: string; + patterns: PatternId[]; +}>; + +export const repeaterConfig: PatternConfig = { + displayName: 'Repeater', + iconPath: 'block-icon.svg', + initial: { + legend: 'Default Heading', + patterns: [], + }, + parseConfigData, + getChildren(pattern, patterns) { + return pattern.data.patterns.map( + (patternId: string) => patterns[patternId] + ); + }, + removeChildPattern(pattern, patternId) { + const newPatterns = pattern.data.patterns.filter( + (id: string) => patternId !== id + ); + if (newPatterns.length === pattern.data.patterns.length) { + return pattern; + } + return { + ...pattern, + data: { + ...pattern.data, + patterns: newPatterns, + }, + }; + }, + createPrompt, +}; diff --git a/packages/forms/src/patterns/repeater/prompt.ts b/packages/forms/src/patterns/repeater/prompt.ts new file mode 100644 index 00000000..671ba634 --- /dev/null +++ b/packages/forms/src/patterns/repeater/prompt.ts @@ -0,0 +1,27 @@ +import { type RepeaterPattern } from './index.js'; +import { + type CreatePrompt, + type RepeaterProps, + createPromptForPattern, + getPattern, +} from '../../index.js'; + +export const createPrompt: CreatePrompt = ( + config, + session, + pattern, + options +) => { + const children = pattern.data.patterns.map((patternId: string) => { + const childPattern = getPattern(session.form, patternId); + return createPromptForPattern(config, session, childPattern, options); + }); + return { + props: { + _patternId: pattern.id, + type: 'repeater', + legend: pattern.data.legend, + } satisfies RepeaterProps, + children, + }; +}; From cbf3c94c90c902b1bb5a5d002c7784d04881c31f Mon Sep 17 00:00:00 2001 From: ethangardner Date: Thu, 26 Sep 2024 17:29:24 -0400 Subject: [PATCH 02/27] add a clone/delete item control for the repeater field to duplicate or remove a set of questions --- .../src/Form/components/Repeater/index.tsx | 73 +++++++++++++++++-- .../FormEdit/AddPatternDropdown.tsx | 73 +++++++++++++++++++ .../components/RepeaterPatternEdit.tsx | 26 ++++--- .../design/src/FormManager/FormEdit/store.ts | 28 +++++++ packages/forms/src/builder/index.ts | 11 +++ packages/forms/src/components.ts | 1 + packages/forms/src/index.ts | 39 ++++++++++ packages/forms/src/patterns/repeater/index.ts | 1 + .../forms/src/patterns/repeater/prompt.ts | 1 + 9 files changed, 234 insertions(+), 19 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index b918e3f0..535c1fda 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -1,20 +1,79 @@ -import React from 'react'; - +import React, { useState } from 'react'; import { type RepeaterProps } from '@atj/forms'; - import { type PatternComponent } from '../../../Form/index.js'; -const Repeater: PatternComponent = props => { +const Repeater: PatternComponent = (props) => { + // Using state to store and manage children elements + const [fields, setFields] = useState([React.Children.toArray(props.children)]); + + // // Load initial state from localStorage if available + // useEffect(() => { + // const storedChildren = localStorage.getItem('repeaterChildren'); + // if (storedChildren) { + // setFields(JSON.parse(storedChildren)); + // } + // }, []); + + // // Sync state with localStorage + // useEffect(() => { + // localStorage.setItem('repeaterChildren', JSON.stringify(children)); + // }, [children]); + + // Handler to clone children + const handleClone = () => { + setFields([...fields, [React.Children.toArray(props.children)]]); + }; + + // Handler to delete children + const handleDelete = (index: number) => { + setFields((fields) => [ + ...fields.slice(0, index), + ...fields.slice(index + 1) + ]); + }; + return (
- {props.legend !== '' && props.legend !== undefined && ( + {props.legend && ( {props.legend} )} + {fields ? ( + <> +
    + {fields.map((item, index) => { + return ( +
  • + {item} + {props.showControls !== false ? ( +

    + +

    + ) : null} +
  • + ); + })} +
+ {props.showControls !== false ? ( +

+ +

+ ) : null} + + ) : null } - {props.children}
); }; -export default Repeater; + +export default Repeater; \ No newline at end of file diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index f7b03f2d..01677049 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -229,6 +229,79 @@ export const FieldsetEmptyStateAddPatternButton = ({ ); }; +export const RepeaterAddPatternButton = ({ + patternSelected, + title, +}: { + patternSelected: (patternType: string) => void; + title: string; +}) => { + const { uswdsRoot } = useFormManagerStore(state => ({ + uswdsRoot: state.context.uswdsRoot, + })); + const [isOpen, setIsOpen] = useState(false); + return ( +
+ setIsOpen(false)} + isOpen={isOpen} + patternSelected={patternSelected} + > + + +
+ ); +}; + +export const RepeaterEmptyStateAddPatternButton = ({ + patternSelected, + title, +}: { + patternSelected: (patternType: string) => void; + title: string; +}) => { + const [isOpen, setIsOpen] = useState(false); + return ( + setIsOpen(false)} + isOpen={isOpen} + patternSelected={patternSelected} + > + + + ); +}; + export const AddPatternDropdown = ({ children, availablePatterns, diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx index de509974..2cf7af36 100644 --- a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -4,10 +4,10 @@ import React from 'react'; import { type PatternId, type RepeaterProps } from '@atj/forms'; import { RepeaterPattern } from '@atj/forms'; -// import { -// RepeaterAddPatternButton, -// RepeaterEmptyStateAddPatternButton, -// } from '../AddPatternDropdown.js'; +import { + RepeaterAddPatternButton, + RepeaterEmptyStateAddPatternButton, +} from '../AddPatternDropdown.js'; import { PatternComponent } from '../../../Form/index.js'; import Repeater from '../../../Form/components/Repeater/index.js'; import { useFormManagerStore } from '../../store.js'; @@ -25,14 +25,12 @@ const RepeaterEdit: PatternEditComponent = ({ return ( <> {focus ? ( -

Edit pattern

+ } + > ) : ( - // } - // > -

Preview pattern

- // + )} ); @@ -45,12 +43,16 @@ const RepeaterPreview: PatternComponent = props => { deletePattern: state.deletePattern, }) ); + const propsOverride = { + ...props, + showControls: false + }; const pattern = useFormManagerStore( state => state.session.form.patterns[props._patternId] ); return ( <> - + {props.children} {pattern && pattern.data.patterns.length === 0 && (
void; addPattern: (patternType: string) => void; addPatternToFieldset: (patternType: string, targetPattern: PatternId) => void; + addPatternToRepeater: (patternType: string, targetPattern: PatternId) => void; clearFocus: () => void; copyPattern: (parentPatternId: PatternId, patternId: PatternId) => void; deletePattern: (id: PatternId) => void; @@ -138,6 +139,33 @@ export const createFormEditSlice = 'Element added to fieldset successfully.' ); }, + addPatternToRepeater: (patternType, targetPattern) => { + const state = get(); + const builder = new BlueprintBuilder( + state.context.config, + state.session.form + ); + const newPattern = builder.addPatternToRepeater( + patternType, + targetPattern + ); + + console.group('form slices'); + console.log({ + session: mergeSession(state.session, { form: builder.form }), + focus: { pattern: newPattern }, + }); + console.groupEnd(); + + set({ + session: mergeSession(state.session, { form: builder.form }), + focus: { pattern: newPattern }, + }); + state.addNotification( + 'success', + 'Element added to fieldset successfully.' + ); + }, clearFocus: () => { set({ focus: undefined }); }, diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index 09e49e7e..e114791e 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -10,6 +10,7 @@ import { addDocument, addPageToPageSet, addPatternToFieldset, + addPatternToRepeater, addPatternToPage, copyPattern, createDefaultPattern, @@ -111,6 +112,16 @@ export class BlueprintBuilder { return pattern; } + addPatternToRepeater(patternType: string, fieldsetPatternId: PatternId) { + const pattern = createDefaultPattern(this.config, patternType); + const root = this.form.patterns[fieldsetPatternId] as FieldsetPattern; + if (root.type !== 'repeater') { + throw new Error('expected pattern to be a fieldset'); + } + this.bp = addPatternToRepeater(this.form, fieldsetPatternId, pattern); + return pattern; + } + removePattern(id: PatternId) { this.bp = removePatternFromBlueprint(this.config, this.bp, id); } diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index 68c0a27d..b6b54a85 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -84,6 +84,7 @@ export type RadioGroupProps = PatternProps<{ export type RepeaterProps = PatternProps<{ type: 'repeater'; legend?: string; + showControls?: boolean; subHeading?: string; error?: FormError; }>; diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index abcfcb9b..dffaf719 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -517,6 +517,45 @@ export const addPatternToFieldset = ( }; }; +export const addPatternToRepeater = ( + bp: Blueprint, + fieldsetPatternId: PatternId, + pattern: Pattern, + index?: number +): Blueprint => { + const fieldsetPattern = bp.patterns[fieldsetPatternId] as FieldsetPattern; + if (fieldsetPattern.type !== 'repeater') { + throw new Error('Pattern is not a repeater.'); + } + + let updatedPagePattern: PatternId[]; + + if (index !== undefined) { + updatedPagePattern = [ + ...fieldsetPattern.data.patterns.slice(0, index + 1), + pattern.id, + ...fieldsetPattern.data.patterns.slice(index + 1), + ]; + } else { + updatedPagePattern = [...fieldsetPattern.data.patterns, pattern.id]; + } + + return { + ...bp, + patterns: { + ...bp.patterns, + [fieldsetPattern.id]: { + ...fieldsetPattern, + data: { + ...fieldsetPattern.data, + patterns: updatedPagePattern, + }, + } satisfies FieldsetPattern, + [pattern.id]: pattern, + }, + }; +}; + export const addPageToPageSet = ( bp: Blueprint, pattern: Pattern diff --git a/packages/forms/src/patterns/repeater/index.ts b/packages/forms/src/patterns/repeater/index.ts index e197a62f..4dee4836 100644 --- a/packages/forms/src/patterns/repeater/index.ts +++ b/packages/forms/src/patterns/repeater/index.ts @@ -8,6 +8,7 @@ import { createPrompt } from './prompt.js'; export type RepeaterPattern = Pattern<{ legend?: string; + showControls?: boolean; patterns: PatternId[]; }>; diff --git a/packages/forms/src/patterns/repeater/prompt.ts b/packages/forms/src/patterns/repeater/prompt.ts index 671ba634..6a998327 100644 --- a/packages/forms/src/patterns/repeater/prompt.ts +++ b/packages/forms/src/patterns/repeater/prompt.ts @@ -21,6 +21,7 @@ export const createPrompt: CreatePrompt = ( _patternId: pattern.id, type: 'repeater', legend: pattern.data.legend, + showControls: true, } satisfies RepeaterProps, children, }; From bf1f21e6776db673b2b76e7ede0a509abca16d9d Mon Sep 17 00:00:00 2001 From: ethangardner Date: Thu, 26 Sep 2024 17:51:11 -0400 Subject: [PATCH 03/27] formatting --- .../src/Form/components/Repeater/index.tsx | 26 ++++++++++++------- .../components/RepeaterPatternEdit.tsx | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 535c1fda..8edad0cb 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -2,9 +2,11 @@ import React, { useState } from 'react'; import { type RepeaterProps } from '@atj/forms'; import { type PatternComponent } from '../../../Form/index.js'; -const Repeater: PatternComponent = (props) => { +const Repeater: PatternComponent = props => { // Using state to store and manage children elements - const [fields, setFields] = useState([React.Children.toArray(props.children)]); + const [fields, setFields] = useState([ + React.Children.toArray(props.children), + ]); // // Load initial state from localStorage if available // useEffect(() => { @@ -26,9 +28,9 @@ const Repeater: PatternComponent = (props) => { // Handler to delete children const handleDelete = (index: number) => { - setFields((fields) => [ + setFields(fields => [ ...fields.slice(0, index), - ...fields.slice(index + 1) + ...fields.slice(index + 1), ]); }; @@ -44,7 +46,10 @@ const Repeater: PatternComponent = (props) => {
    {fields.map((item, index) => { return ( -
  • +
  • {item} {props.showControls !== false ? (

    @@ -64,16 +69,19 @@ const Repeater: PatternComponent = (props) => {

{props.showControls !== false ? (

-

) : null} - ) : null } - + ) : null} ); }; -export default Repeater; \ No newline at end of file +export default Repeater; diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx index 2cf7af36..244da381 100644 --- a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -45,7 +45,7 @@ const RepeaterPreview: PatternComponent = props => { ); const propsOverride = { ...props, - showControls: false + showControls: false, }; const pattern = useFormManagerStore( state => state.session.form.patterns[props._patternId] From b42cc6a6d7c3287d7a57118c051af136def26de3 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 27 Sep 2024 16:22:45 -0400 Subject: [PATCH 04/27] add presentational component for edit view --- .../src/Form/components/Repeater/edit.tsx | 19 +++++++++ .../src/Form/components/Repeater/index.tsx | 42 +++++++++---------- .../components/RepeaterPatternEdit.tsx | 9 ++-- 3 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 packages/design/src/Form/components/Repeater/edit.tsx diff --git a/packages/design/src/Form/components/Repeater/edit.tsx b/packages/design/src/Form/components/Repeater/edit.tsx new file mode 100644 index 00000000..b69e09ef --- /dev/null +++ b/packages/design/src/Form/components/Repeater/edit.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { type RepeaterProps } from '@atj/forms'; +import { type PatternComponent } from '../../../Form/index.js'; + +const RepeaterEditView: PatternComponent = props => { + return ( +
+ {props.legend !== '' && props.legend !== undefined && ( + + {props.legend} + + )} + + {props.children} +
+ ); +}; + +export default RepeaterEditView; diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 8edad0cb..ef7cf7e8 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -51,33 +51,29 @@ const Repeater: PatternComponent = props => { className="padding-bottom-2 border-bottom border-base-lighter" > {item} - {props.showControls !== false ? ( -

- -

- ) : null} +

+ +

); })} - {props.showControls !== false ? ( -

- -

- ) : null} +

+ +

) : null} diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx index 244da381..bc58b5f1 100644 --- a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -10,6 +10,7 @@ import { } from '../AddPatternDropdown.js'; import { PatternComponent } from '../../../Form/index.js'; import Repeater from '../../../Form/components/Repeater/index.js'; +import RepeaterEditView from '../../../Form/components/Repeater/edit.js'; import { useFormManagerStore } from '../../store.js'; import { PatternEditComponent } from '../types.js'; @@ -43,16 +44,12 @@ const RepeaterPreview: PatternComponent = props => { deletePattern: state.deletePattern, }) ); - const propsOverride = { - ...props, - showControls: false, - }; const pattern = useFormManagerStore( state => state.session.form.patterns[props._patternId] ); return ( <> - + {props.children} {pattern && pattern.data.patterns.length === 0 && (
= props => {
)} -
+ ); }; From 06935377ec6c1b7c5eda9d17260a89b48951d3c6 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 27 Sep 2024 17:25:52 -0400 Subject: [PATCH 05/27] prevent duplicate ids for input fields. Will need to map canonical id prop for other field types --- .../src/Form/components/Repeater/index.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index ef7cf7e8..dbc5f42a 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -21,9 +21,35 @@ const Repeater: PatternComponent = props => { // localStorage.setItem('repeaterChildren', JSON.stringify(children)); // }, [children]); - // Handler to clone children + // TODO: need to make this work for non-input types. + const cloneWithModifiedId = (children: React.ReactNode[], suffix: number) => { + return React.Children.map(children, (child) => { + if ( + React.isValidElement(child) && + child?.props?.component?.props?.inputId + ) { + // Clone element with modified _patternId + return React.cloneElement(child, { + component: { + ...child.props.component, + props: { + ...child.props.component.props, + inputId: `${child.props.component.props.inputId}_${suffix}`, + }, + }, + }); + } + return child; + }); + }; + const handleClone = () => { - setFields([...fields, [React.Children.toArray(props.children)]]); + const newSuffix = fields.length + 1; // Suffix based on number of existing items + const clonedChildren = cloneWithModifiedId( + React.Children.toArray(props.children), + newSuffix + ); + setFields([...fields, clonedChildren]); }; // Handler to delete children From e80d560b7d369e588f0bb379317d26bf319b1780 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Mon, 30 Sep 2024 17:06:29 -0400 Subject: [PATCH 06/27] use local storage for storing repeater options on the client --- .../src/Form/components/Repeater/index.tsx | 104 +++++++----------- 1 file changed, 40 insertions(+), 64 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index dbc5f42a..7b6ab437 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -1,65 +1,41 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { type RepeaterProps } from '@atj/forms'; import { type PatternComponent } from '../../../Form/index.js'; const Repeater: PatternComponent = props => { - // Using state to store and manage children elements + const STORAGE_KEY = `repeater-${props._patternId}`; + + const loadInitialFields = (): number => { + const storedFields = localStorage.getItem(STORAGE_KEY); + if (storedFields) { + return parseInt(JSON.parse(storedFields), 10) || 1; + } + return 1; + }; + + const [fieldCount, setFieldCount] = useState(loadInitialFields); const [fields, setFields] = useState([ React.Children.toArray(props.children), ]); + const hasFields = React.Children.toArray(props.children).length > 0; - // // Load initial state from localStorage if available - // useEffect(() => { - // const storedChildren = localStorage.getItem('repeaterChildren'); - // if (storedChildren) { - // setFields(JSON.parse(storedChildren)); - // } - // }, []); - - // // Sync state with localStorage - // useEffect(() => { - // localStorage.setItem('repeaterChildren', JSON.stringify(children)); - // }, [children]); - - // TODO: need to make this work for non-input types. - const cloneWithModifiedId = (children: React.ReactNode[], suffix: number) => { - return React.Children.map(children, (child) => { - if ( - React.isValidElement(child) && - child?.props?.component?.props?.inputId - ) { - // Clone element with modified _patternId - return React.cloneElement(child, { - component: { - ...child.props.component, - props: { - ...child.props.component.props, - inputId: `${child.props.component.props.inputId}_${suffix}`, - }, - }, - }); - } - return child; - }); - }; + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.parse(fieldCount.toString())); + setFields( + new Array(fieldCount).fill(React.Children.toArray(props.children)) + ); + }, [fieldCount]); const handleClone = () => { - const newSuffix = fields.length + 1; // Suffix based on number of existing items - const clonedChildren = cloneWithModifiedId( - React.Children.toArray(props.children), - newSuffix - ); - setFields([...fields, clonedChildren]); + setFieldCount(fieldCount => fieldCount + 1); }; - // Handler to delete children - const handleDelete = (index: number) => { - setFields(fields => [ - ...fields.slice(0, index), - ...fields.slice(index + 1), - ]); + const handleDelete = () => { + setFieldCount(fieldCount => fieldCount - 1); }; + // TODO: prevent duplicate ID attributes when items are cloned + return (
{props.legend && ( @@ -67,31 +43,21 @@ const Repeater: PatternComponent = props => { {props.legend} )} - {fields ? ( + {hasFields ? ( <> -
    +
      {fields.map((item, index) => { return (
    • {item} -

      - -

    • ); })}
    -

    +

    -

    + +
    - ) : null} + ) : ( +

    This fieldset

    + )}
); }; From 9dae538cf4a90ca159ad4d3df7c57ae98546ded5 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Mon, 30 Sep 2024 17:46:37 -0400 Subject: [PATCH 07/27] add function to mutate ids for cloned elements. need to make it work for all input types. : --- .../src/Form/components/Repeater/index.tsx | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 7b6ab437..f4606ac6 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -34,7 +34,28 @@ const Repeater: PatternComponent = props => { setFieldCount(fieldCount => fieldCount - 1); }; - // TODO: prevent duplicate ID attributes when items are cloned + // TODO: need to make this work for non-input types. + const renderWithUniqueIds = (children: React.ReactNode, index: number) => { + return React.Children.map(children, (child) => { + if ( + React.isValidElement(child) && + child?.props?.component?.props?.inputId + ) { + // Clone element with modified _patternId + return React.cloneElement(child, { + component: { + ...child.props.component, + props: { + ...child.props.component.props, + inputId: `${child.props.component.props.inputId}_${index}`, + }, + }, + }); + } + return child; + }); + }; + return (
@@ -52,7 +73,7 @@ const Repeater: PatternComponent = props => { key={index} className="padding-bottom-4 border-bottom border-base-lighter" > - {item} + {renderWithUniqueIds(item, index)} ); })} @@ -76,10 +97,16 @@ const Repeater: PatternComponent = props => { ) : ( -

This fieldset

- )} -
- ); -}; +
+
+

+ This fieldset does not have any items assigned to it. +

+
+
+ )} + + ); + }; -export default Repeater; + export default Repeater; From 2e16abe22049e977da6106a3a31c27c59645d975 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Mon, 30 Sep 2024 17:49:15 -0400 Subject: [PATCH 08/27] formatting --- .../design/src/Form/components/Repeater/index.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index f4606ac6..d89295fb 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -36,7 +36,7 @@ const Repeater: PatternComponent = props => { // TODO: need to make this work for non-input types. const renderWithUniqueIds = (children: React.ReactNode, index: number) => { - return React.Children.map(children, (child) => { + return React.Children.map(children, child => { if ( React.isValidElement(child) && child?.props?.component?.props?.inputId @@ -56,7 +56,6 @@ const Repeater: PatternComponent = props => { }); }; - return (
{props.legend && ( @@ -104,9 +103,9 @@ const Repeater: PatternComponent = props => {

- )} -
- ); - }; + )} + + ); +}; - export default Repeater; +export default Repeater; From c9bf5d27743c36decc37bb37612a03542ca55e9d Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 1 Oct 2024 12:10:21 -0400 Subject: [PATCH 09/27] render update radio group components id in repeater --- .../src/Form/components/Repeater/index.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index d89295fb..0ad73227 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -39,18 +39,33 @@ const Repeater: PatternComponent = props => { return React.Children.map(children, child => { if ( React.isValidElement(child) && - child?.props?.component?.props?.inputId + child?.props?.component?.props ) { - // Clone element with modified _patternId - return React.cloneElement(child, { - component: { - ...child.props.component, - props: { - ...child.props.component.props, - inputId: `${child.props.component.props.inputId}_${index}`, + if ( + child.props.component.props.type === 'input' && + child.props.component.props.inputId + ) { + return React.cloneElement(child, { + component: { + ...child.props.component, + props: { + ...child.props.component.props, + inputId: `${child.props.component.props.inputId}_${index}`, + }, }, - }, - }); + }); + } else if (child.props.component.props.type === 'radio-group') { + console.log(child.props.component.props.options); + return React.cloneElement(child, { + component: { + ...child.props.component, + props: { + ...child.props.component.props, + groupId: `${child.props.component.props.groupId}_${index}`, + }, + }, + }); + } } return child; }); From a00acb87368516e3c913444c6a8fafc9afc1866b Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 1 Oct 2024 12:45:55 -0400 Subject: [PATCH 10/27] remove empty test language from user-facing component --- .../design/src/Form/components/Repeater/index.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 0ad73227..27028aec 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -37,10 +37,7 @@ const Repeater: PatternComponent = props => { // TODO: need to make this work for non-input types. const renderWithUniqueIds = (children: React.ReactNode, index: number) => { return React.Children.map(children, child => { - if ( - React.isValidElement(child) && - child?.props?.component?.props - ) { + if (React.isValidElement(child) && child?.props?.component?.props) { if ( child.props.component.props.type === 'input' && child.props.component.props.inputId @@ -110,15 +107,7 @@ const Repeater: PatternComponent = props => { - ) : ( -
-
-

- This fieldset does not have any items assigned to it. -

-
-
- )} + ) : null} ); }; From 910faa41b42ff9620bb0bca46fd8b35d08f1e7d9 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Wed, 2 Oct 2024 10:07:48 -0400 Subject: [PATCH 11/27] update ids have an optional suffix to ensure unique ids in the repeater field --- .../Form/components/RadioGroup/RadioGroup.tsx | 9 ++++-- .../src/Form/components/Repeater/index.tsx | 32 +++++-------------- .../src/Form/components/TextInput/index.tsx | 13 +++++--- packages/forms/src/components.ts | 3 ++ 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/design/src/Form/components/RadioGroup/RadioGroup.tsx b/packages/design/src/Form/components/RadioGroup/RadioGroup.tsx index 663302f7..686f8d3b 100644 --- a/packages/design/src/Form/components/RadioGroup/RadioGroup.tsx +++ b/packages/design/src/Form/components/RadioGroup/RadioGroup.tsx @@ -13,17 +13,20 @@ export const RadioGroupPattern: PatternComponent = props => { {props.legend} {props.options.map((option, index) => { + const id = props.idSuffix ? `${option.id}${props.idSuffix}` : option.id; return (
-
diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 27028aec..6b30387b 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -38,31 +38,15 @@ const Repeater: PatternComponent = props => { const renderWithUniqueIds = (children: React.ReactNode, index: number) => { return React.Children.map(children, child => { if (React.isValidElement(child) && child?.props?.component?.props) { - if ( - child.props.component.props.type === 'input' && - child.props.component.props.inputId - ) { - return React.cloneElement(child, { - component: { - ...child.props.component, - props: { - ...child.props.component.props, - inputId: `${child.props.component.props.inputId}_${index}`, - }, + return React.cloneElement(child, { + component: { + ...child.props.component, + props: { + ...child.props.component.props, + idSuffix: `_${index}`, }, - }); - } else if (child.props.component.props.type === 'radio-group') { - console.log(child.props.component.props.options); - return React.cloneElement(child, { - component: { - ...child.props.component, - props: { - ...child.props.component.props, - groupId: `${child.props.component.props.groupId}_${index}`, - }, - }, - }); - } + }, + }); } return child; }); diff --git a/packages/design/src/Form/components/TextInput/index.tsx b/packages/design/src/Form/components/TextInput/index.tsx index da57726a..1b875902 100644 --- a/packages/design/src/Form/components/TextInput/index.tsx +++ b/packages/design/src/Form/components/TextInput/index.tsx @@ -7,6 +7,9 @@ import { type PatternComponent } from '../../../Form/index.js'; const TextInput: PatternComponent = props => { const { register } = useFormContext(); + const id = props.idSuffix + ? `${props.inputId}${props.idSuffix}` + : props.inputId; return (
= props => { className={classNames('usa-label', { 'usa-label--error': props.error, })} - id={`input-message-${props.inputId}`} + id={`input-message-${id}`} > {props.label} {props.error && ( {props.error.message} @@ -34,13 +37,13 @@ const TextInput: PatternComponent = props => { className={classNames('usa-input', { 'usa-input--error': props.error, })} - id={`input-${props.inputId}`} + id={`input-${id}`} defaultValue={props.value} - {...register(props.inputId || Math.random().toString(), { + {...register(id || Math.random().toString(), { //required: props.required, })} type="text" - aria-describedby={`input-message-${props.inputId}`} + aria-describedby={`input-message-${id}`} />
diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index b6b54a85..a6b8e17c 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -10,6 +10,7 @@ import { type FormSession, nullSession, sessionIsComplete } from './session.js'; export type TextInputProps = PatternProps<{ type: 'input'; inputId: string; + idSuffix?: string; value: string; label: string; required: boolean; @@ -54,6 +55,7 @@ export type ZipcodeProps = PatternProps<{ export type CheckboxProps = PatternProps<{ type: 'checkbox'; id: string; + idSuffix?: string; label: string; defaultChecked: boolean; }>; @@ -73,6 +75,7 @@ export type RadioGroupProps = PatternProps<{ type: 'radio-group'; groupId: string; legend: string; + idSuffix?: string; options: { id: string; name: string; From ce0b362cd4a510353c1c493ef84d0a5a89da5d98 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Thu, 3 Oct 2024 15:04:05 -0400 Subject: [PATCH 12/27] sensible default for local storage --- packages/design/src/Form/components/Repeater/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index 6b30387b..bd8e20c4 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -20,7 +20,12 @@ const Repeater: PatternComponent = props => { const hasFields = React.Children.toArray(props.children).length > 0; useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.parse(fieldCount.toString())); + if ( + (!localStorage.getItem(STORAGE_KEY) && fieldCount !== 1) || + localStorage.getItem(STORAGE_KEY) + ) { + localStorage.setItem(STORAGE_KEY, JSON.parse(fieldCount.toString())); + } setFields( new Array(fieldCount).fill(React.Children.toArray(props.children)) ); From cef3078c69e4b8fef82beccf8972a323a61a9509 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 4 Oct 2024 11:26:18 -0400 Subject: [PATCH 13/27] add function to get id for pattern --- packages/forms/src/response.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index 7018654a..eb9cf9cb 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -39,7 +39,8 @@ const parsePromptResponse = ( const values: Record = {}; const errors: FormErrorMap = {}; for (const [patternId, promptValue] of Object.entries(response.data)) { - const pattern = getPattern(session.form, patternId); + const id = getPatternId(patternId); + const pattern = getPattern(session.form, id); const patternConfig = getPatternConfig(config, pattern.type); const isValidResult = validatePattern(patternConfig, pattern, promptValue); if (isValidResult.success) { @@ -50,3 +51,7 @@ const parsePromptResponse = ( } return { errors, values }; }; + +const getPatternId = (id: string) => { + return id.replace(/_repeater_(\d+)$/, ''); +}; From 0fc3b208b7bc3fa400a50b2ba763a4760ee385f2 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 4 Oct 2024 14:30:56 -0400 Subject: [PATCH 14/27] update id modifier string --- packages/design/src/Form/components/Repeater/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index bd8e20c4..d0369dd4 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -48,7 +48,7 @@ const Repeater: PatternComponent = props => { ...child.props.component, props: { ...child.props.component.props, - idSuffix: `_${index}`, + idSuffix: `_repeater_${index}`, }, }, }); From 2a5badebfb00f8ea5bdf354f79b4c742041690d3 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 4 Oct 2024 15:13:51 -0400 Subject: [PATCH 15/27] clean up pattern logic for dropdown --- .../src/FormManager/FormEdit/AddPatternDropdown.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index 01677049..2050fe7b 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -95,13 +95,9 @@ const sidebarPatterns: DropdownPattern[] = [ ['repeater', defaultFormConfig.patterns['repeater']], ['radio-group', defaultFormConfig.patterns['radio-group']], ] as const; -export const fieldsetPatterns: DropdownPattern[] = [ - ['form-summary', defaultFormConfig.patterns['form-summary']], - ['input', defaultFormConfig.patterns['input']], - ['paragraph', defaultFormConfig.patterns['paragraph']], - ['rich-text', defaultFormConfig.patterns['rich-text']], - ['radio-group', defaultFormConfig.patterns['radio-group']], -] as const; +export const fieldsetPatterns: DropdownPattern[] = sidebarPatterns.filter( + ([key]) => key !== 'fieldset' && key !== 'repeater' +); export const SidebarAddPatternMenuItem = ({ patternSelected, From 31236b131f36c2ec96138a9174d5ae61133fc85e Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 8 Oct 2024 10:54:20 -0400 Subject: [PATCH 16/27] refactor to use react hook form useFieldsArray --- .../src/Form/components/Repeater/index.tsx | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index d0369dd4..e23ee104 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -1,4 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import { useFieldArray, useForm } from 'react-hook-form'; import { type RepeaterProps } from '@atj/forms'; import { type PatternComponent } from '../../../Form/index.js'; @@ -13,33 +14,23 @@ const Repeater: PatternComponent = props => { return 1; }; - const [fieldCount, setFieldCount] = useState(loadInitialFields); - const [fields, setFields] = useState([ - React.Children.toArray(props.children), - ]); - const hasFields = React.Children.toArray(props.children).length > 0; + const { control } = useForm({ + defaultValues: { + fields: Array(loadInitialFields()).fill({}), + }, + }); - useEffect(() => { - if ( - (!localStorage.getItem(STORAGE_KEY) && fieldCount !== 1) || - localStorage.getItem(STORAGE_KEY) - ) { - localStorage.setItem(STORAGE_KEY, JSON.parse(fieldCount.toString())); - } - setFields( - new Array(fieldCount).fill(React.Children.toArray(props.children)) - ); - }, [fieldCount]); + const { fields, append, remove } = useFieldArray({ + control, + name: 'fields', + }); - const handleClone = () => { - setFieldCount(fieldCount => fieldCount + 1); - }; + React.useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(fields.length)); + }, [fields.length]); - const handleDelete = () => { - setFieldCount(fieldCount => fieldCount - 1); - }; + const hasFields = React.Children.toArray(props.children).length > 0; - // TODO: need to make this work for non-input types. const renderWithUniqueIds = (children: React.ReactNode, index: number) => { return React.Children.map(children, child => { if (React.isValidElement(child) && child?.props?.component?.props) { @@ -48,7 +39,7 @@ const Repeater: PatternComponent = props => { ...child.props.component, props: { ...child.props.component.props, - idSuffix: `_repeater_${index}`, + idSuffix: `.repeater.${index}`, }, }, }); @@ -64,39 +55,37 @@ const Repeater: PatternComponent = props => { {props.legend} )} - {hasFields ? ( + {hasFields && ( <>
    - {fields.map((item, index) => { - return ( -
  • - {renderWithUniqueIds(item, index)} -
  • - ); - })} + {fields.map((field, index) => ( +
  • + {renderWithUniqueIds(props.children, index)} +
  • + ))}
- ) : null} + )} ); }; From 8129a7f2e5eeb34cf0cb56c0a73ee6804eb3db52 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 8 Oct 2024 17:34:01 -0400 Subject: [PATCH 17/27] work in progress on repeater validation and structure --- packages/forms/src/pattern.ts | 5 +++ packages/forms/src/patterns/repeater/index.ts | 35 +++++++++++++++++++ packages/forms/src/response.ts | 5 +++ packages/forms/src/session.ts | 4 +++ packages/forms/src/util/zod.ts | 4 +++ 5 files changed, 53 insertions(+) diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index dcd1cb64..3bbb4848 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -79,6 +79,11 @@ export const validatePattern = ( pattern: Pattern, value: any ): r.Result => { + console.group('validatePattern'); + console.log(pattern); + console.log(patternConfig); + console.log(value); + console.groupEnd(); if (!patternConfig.parseUserInput) { return { success: true, diff --git a/packages/forms/src/patterns/repeater/index.ts b/packages/forms/src/patterns/repeater/index.ts index 4dee4836..568b5812 100644 --- a/packages/forms/src/patterns/repeater/index.ts +++ b/packages/forms/src/patterns/repeater/index.ts @@ -20,6 +20,41 @@ export const repeaterConfig: PatternConfig = { patterns: [], }, parseConfigData, + parseUserInput: (pattern, input: unknown) => { + console.group('parseUserInput'); + console.log(pattern); + console.log(input); + console.groupEnd(); + + // 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}`, + }, + }; + */ + }, getChildren(pattern, patterns) { return pattern.data.patterns.map( (patternId: string) => patterns[patternId] diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index eb9cf9cb..1fdaf29c 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -43,6 +43,11 @@ const parsePromptResponse = ( const pattern = getPattern(session.form, id); const patternConfig = getPatternConfig(config, pattern.type); const isValidResult = validatePattern(patternConfig, pattern, promptValue); + console.group('parsePromptResponse'); + console.log(pattern); + console.log(patternConfig); + console.log(isValidResult); + console.groupEnd(); if (isValidResult.success) { values[patternId] = isValidResult.data; } else { diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index 84083c53..df230f5a 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -116,6 +116,10 @@ export const updateSession = ( values: PatternValueMap, errors: FormErrorMap ): FormSession => { + console.group('updateSession'); + console.log('values', values); + console.log('errors', errors); + console.groupEnd(); const keysValid = Object.keys(values).every( patternId => patternId in session.form.patterns diff --git a/packages/forms/src/util/zod.ts b/packages/forms/src/util/zod.ts index 097b0437..ce2d34a1 100644 --- a/packages/forms/src/util/zod.ts +++ b/packages/forms/src/util/zod.ts @@ -33,6 +33,10 @@ export const safeZodParseFormErrors = ( schema: Schema, obj: unknown ): r.Result, FormErrors> => { + // console.group('safeZodParseFormErrors'); + // console.log(schema); + // console.log(obj); + // console.groupEnd(); const result = safeZodParse(schema, obj); if (result.success) { return r.success(result.data); From 63b47ec069f52c79d29d49ffda3799ccea94945d Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 8 Oct 2024 01:10:25 -0400 Subject: [PATCH 18/27] ignore .idea dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0f41fb70..41dc02f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ _site .turbo/ .vscode/ +.idea/ coverage/ html/ node_modules/ From a7349f81b571588de52859d18f7dd373fb9fa710 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 8 Oct 2024 21:41:46 -0400 Subject: [PATCH 19/27] dry out add pattern dropdown functions --- .../FormEdit/AddPatternDropdown.tsx | 77 +------------------ .../FormEdit/components/FieldsetEdit.tsx | 8 +- .../components/RepeaterPatternEdit.tsx | 8 +- 3 files changed, 10 insertions(+), 83 deletions(-) diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index 2050fe7b..5f02776e 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -152,7 +152,7 @@ export const SidebarAddPatternMenuItem = ({ ); }; -export const FieldsetAddPatternButton = ({ +export const CompoundAddPatternButton = ({ patternSelected, title, }: { @@ -200,80 +200,7 @@ export const FieldsetAddPatternButton = ({ ); }; -export const FieldsetEmptyStateAddPatternButton = ({ - patternSelected, - title, -}: { - patternSelected: (patternType: string) => void; - title: string; -}) => { - const [isOpen, setIsOpen] = useState(false); - return ( - setIsOpen(false)} - isOpen={isOpen} - patternSelected={patternSelected} - > - - - ); -}; - -export const RepeaterAddPatternButton = ({ - patternSelected, - title, -}: { - patternSelected: (patternType: string) => void; - title: string; -}) => { - const { uswdsRoot } = useFormManagerStore(state => ({ - uswdsRoot: state.context.uswdsRoot, - })); - const [isOpen, setIsOpen] = useState(false); - return ( -
- setIsOpen(false)} - isOpen={isOpen} - patternSelected={patternSelected} - > - - -
- ); -}; - -export const RepeaterEmptyStateAddPatternButton = ({ +export const CompoundAddNewPatternButton = ({ patternSelected, title, }: { diff --git a/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx b/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx index b5e944d3..932f1f2f 100644 --- a/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx @@ -5,8 +5,8 @@ import { type PatternId, type FieldsetProps } from '@atj/forms'; import { FieldsetPattern } from '@atj/forms'; import { - FieldsetAddPatternButton, - FieldsetEmptyStateAddPatternButton, + CompoundAddPatternButton, + CompoundAddNewPatternButton, } from '../AddPatternDropdown.js'; import { PatternComponent } from '../../../Form/index.js'; import Fieldset from '../../../Form/components/Fieldset/index.js'; @@ -61,7 +61,7 @@ const FieldsetPreview: PatternComponent = props => { Empty sections will not display. - addPatternToFieldset(patternType, props._patternId) @@ -88,7 +88,7 @@ const FieldsetPreview: PatternComponent = props => { className="margin-left-3 margin-right-3 margin-bottom-3 bg-none" >
- addPatternToFieldset(patternType, props._patternId) diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx index bc58b5f1..77d1bb7b 100644 --- a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -5,8 +5,8 @@ import { type PatternId, type RepeaterProps } from '@atj/forms'; import { RepeaterPattern } from '@atj/forms'; import { - RepeaterAddPatternButton, - RepeaterEmptyStateAddPatternButton, + CompoundAddPatternButton, + CompoundAddNewPatternButton, } from '../AddPatternDropdown.js'; import { PatternComponent } from '../../../Form/index.js'; import Repeater from '../../../Form/components/Repeater/index.js'; @@ -62,7 +62,7 @@ const RepeaterPreview: PatternComponent = props => { Empty sections will not display. - addPatternToRepeater(patternType, props._patternId) @@ -89,7 +89,7 @@ const RepeaterPreview: PatternComponent = props => { className="margin-left-3 margin-right-3 margin-bottom-3 bg-none" >
- addPatternToRepeater(patternType, props._patternId) From a801e8c822cbeaed03706f713fd79983b7062f16 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Tue, 8 Oct 2024 22:10:15 -0400 Subject: [PATCH 20/27] refactor dropdown buttons and consolidate prop types --- .../FormEdit/AddPatternDropdown.tsx | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index 5f02776e..5de1962b 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -1,9 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; - import { defaultFormConfig, type PatternConfig } from '@atj/forms'; - import { useFormManagerStore } from '../store.js'; - import styles from './formEditStyles.module.css'; import blockIcon from './images/block-icon.svg'; import checkboxIcon from './images/checkbox-icon.svg'; @@ -22,7 +19,7 @@ import classNames from 'classnames'; const icons: Record = { 'block-icon.svg': blockIcon, 'checkbox-icon.svg': checkboxIcon, - 'date-icon.svg.svg': dateIcon, + 'date-icon.svg': dateIcon, 'dropdown-icon.svg': dropDownIcon, 'dropdownoption-icon.svg': dropDownOptionIcon, 'richtext-icon.svg': richTextIcon, @@ -37,9 +34,14 @@ const getIconPath = (iconPath: string) => { return Object.values(icons[iconPath])[0] as string; }; +interface PatternMenuProps { + patternSelected: (patternType: string) => void; + title: string; +} + export const AddPatternMenu = () => { - const addPage = useFormManagerStore(state => state.addPage); - const { addPattern } = useFormManagerStore(state => ({ + const { addPage, addPattern } = useFormManagerStore(state => ({ + addPage: state.addPage, addPattern: state.addPattern, })); @@ -59,25 +61,11 @@ export const AddPatternMenu = () => { />
  • - +
  • @@ -85,6 +73,34 @@ export const AddPatternMenu = () => { ); }; +const MenuItemButton = ({ + title, + onClick, + iconPath, +}: { + title: string; + onClick: () => void; + iconPath: string; +}) => ( + +); + type DropdownPattern = [string, PatternConfig]; const sidebarPatterns: DropdownPattern[] = [ ['form-summary', defaultFormConfig.patterns['form-summary']], @@ -102,10 +118,7 @@ export const fieldsetPatterns: DropdownPattern[] = sidebarPatterns.filter( export const SidebarAddPatternMenuItem = ({ patternSelected, title, -}: { - patternSelected: (patternType: string) => void; - title: string; -}) => { +}: PatternMenuProps) => { const [isOpen, setIsOpen] = useState(false); const { uswdsRoot } = useFormManagerStore(state => ({ uswdsRoot: state.context.uswdsRoot, @@ -134,7 +147,6 @@ export const SidebarAddPatternMenuItem = ({
    - {title} @@ -155,14 +167,12 @@ export const SidebarAddPatternMenuItem = ({ export const CompoundAddPatternButton = ({ patternSelected, title, -}: { - patternSelected: (patternType: string) => void; - title: string; -}) => { +}: PatternMenuProps) => { const { uswdsRoot } = useFormManagerStore(state => ({ uswdsRoot: state.context.uswdsRoot, })); const [isOpen, setIsOpen] = useState(false); + return (
    void; - title: string; -}) => { +}: PatternMenuProps) => { const [isOpen, setIsOpen] = useState(false); return ( Date: Wed, 9 Oct 2024 13:54:35 -0400 Subject: [PATCH 21/27] update validation to accommodate an array of objects --- packages/forms/src/patterns/input/response.ts | 22 ++++++++++++++----- packages/forms/src/response.ts | 10 ++++----- packages/forms/src/session.ts | 8 +++---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/forms/src/patterns/input/response.ts b/packages/forms/src/patterns/input/response.ts index 30ac0b64..5537886a 100644 --- a/packages/forms/src/patterns/input/response.ts +++ b/packages/forms/src/patterns/input/response.ts @@ -6,11 +6,19 @@ import { safeZodParseToFormError } from '../../util/zod.js'; import { type InputPattern } from './index.js'; const createSchema = (data: InputPattern['data']) => { - const schema = z.string().max(data.maxLength); - if (!data.required) { - return schema; - } - return schema.min(1, { message: 'This field is required' }); + const stringSchema = z.string().max(data.maxLength); + + const baseSchema = data.required + ? stringSchema.min(1, { message: 'This field is required' }) + : stringSchema; + + // Using z.union to handle both single string and object with `repeater` array of strings + return z.union([ + baseSchema, + z.object({ + repeater: z.array(baseSchema), + }), + ]); }; export type InputPatternOutput = z.infer>; @@ -19,5 +27,9 @@ export const parseUserInput: ParseUserInput< InputPattern, InputPatternOutput > = (pattern, obj) => { + // console.group('parseUserInput'); + // console.log(pattern); + // console.log(obj); + // console.groupEnd(); return safeZodParseToFormError(createSchema(pattern['data']), obj); }; diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index 1fdaf29c..f7d5e01f 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -43,11 +43,11 @@ const parsePromptResponse = ( const pattern = getPattern(session.form, id); const patternConfig = getPatternConfig(config, pattern.type); const isValidResult = validatePattern(patternConfig, pattern, promptValue); - console.group('parsePromptResponse'); - console.log(pattern); - console.log(patternConfig); - console.log(isValidResult); - console.groupEnd(); + // console.group('parsePromptResponse'); + // console.log(pattern); + // console.log(patternConfig); + // console.log(isValidResult); + // console.groupEnd(); if (isValidResult.success) { values[patternId] = isValidResult.data; } else { diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index df230f5a..36d604c8 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -116,10 +116,10 @@ export const updateSession = ( values: PatternValueMap, errors: FormErrorMap ): FormSession => { - console.group('updateSession'); - console.log('values', values); - console.log('errors', errors); - console.groupEnd(); + // console.group('updateSession'); + // console.log('values', values); + // console.log('errors', errors); + // console.groupEnd(); const keysValid = Object.keys(values).every( patternId => patternId in session.form.patterns From dbff9f818fcbb2dd030e132cbbe613c20053430e Mon Sep 17 00:00:00 2001 From: ethangardner Date: Wed, 9 Oct 2024 16:29:35 -0400 Subject: [PATCH 22/27] turn off results summary table for now --- .../SubmissionConfirmation/index.tsx | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/design/src/Form/components/SubmissionConfirmation/index.tsx b/packages/design/src/Form/components/SubmissionConfirmation/index.tsx index ed863d74..5d109119 100644 --- a/packages/design/src/Form/components/SubmissionConfirmation/index.tsx +++ b/packages/design/src/Form/components/SubmissionConfirmation/index.tsx @@ -6,6 +6,9 @@ import { type PatternComponent } from '../../../Form/index.js'; const SubmissionConfirmation: PatternComponent< SubmissionConfirmationProps > = props => { + console.group('SubmissionConfirmation'); + console.log(props); + console.groupEnd(); return ( <> @@ -39,30 +42,35 @@ const SubmissionConfirmation: PatternComponent< Submission details - + {/* + EG: turn this off for now. Will need some design perhaps to see what the presentation + should look like. This was a minimal blocker for the repeater field due to the flat data structure + that was there previously. + */} + {/*
    */}
    ); From 9c5b3554db7898c2ca6a7c9ef7a2d1aeed0b8901 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Thu, 10 Oct 2024 15:41:07 -0400 Subject: [PATCH 23/27] remove debugging and console statements --- .../Form/components/SubmissionConfirmation/index.tsx | 5 +---- packages/design/src/FormManager/FormEdit/store.ts | 10 +--------- packages/forms/src/pattern.ts | 5 ----- packages/forms/src/patterns/input/response.ts | 4 ---- packages/forms/src/patterns/repeater/index.ts | 5 ----- packages/forms/src/response.ts | 5 ----- packages/forms/src/session.ts | 4 ---- packages/forms/src/util/zod.ts | 4 ---- 8 files changed, 2 insertions(+), 40 deletions(-) diff --git a/packages/design/src/Form/components/SubmissionConfirmation/index.tsx b/packages/design/src/Form/components/SubmissionConfirmation/index.tsx index 5d109119..d4e9d97b 100644 --- a/packages/design/src/Form/components/SubmissionConfirmation/index.tsx +++ b/packages/design/src/Form/components/SubmissionConfirmation/index.tsx @@ -5,10 +5,7 @@ import { type PatternComponent } from '../../../Form/index.js'; const SubmissionConfirmation: PatternComponent< SubmissionConfirmationProps -> = props => { - console.group('SubmissionConfirmation'); - console.log(props); - console.groupEnd(); +> = (/* props */) => { return ( <> diff --git a/packages/design/src/FormManager/FormEdit/store.ts b/packages/design/src/FormManager/FormEdit/store.ts index e4d8bf7f..e297d5f6 100644 --- a/packages/design/src/FormManager/FormEdit/store.ts +++ b/packages/design/src/FormManager/FormEdit/store.ts @@ -149,21 +149,13 @@ export const createFormEditSlice = patternType, targetPattern ); - - console.group('form slices'); - console.log({ - session: mergeSession(state.session, { form: builder.form }), - focus: { pattern: newPattern }, - }); - console.groupEnd(); - set({ session: mergeSession(state.session, { form: builder.form }), focus: { pattern: newPattern }, }); state.addNotification( 'success', - 'Element added to fieldset successfully.' + 'Element added to repeater successfully.' ); }, clearFocus: () => { diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index 3bbb4848..dcd1cb64 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -79,11 +79,6 @@ export const validatePattern = ( pattern: Pattern, value: any ): r.Result => { - console.group('validatePattern'); - console.log(pattern); - console.log(patternConfig); - console.log(value); - console.groupEnd(); if (!patternConfig.parseUserInput) { return { success: true, diff --git a/packages/forms/src/patterns/input/response.ts b/packages/forms/src/patterns/input/response.ts index 5537886a..d455e8ce 100644 --- a/packages/forms/src/patterns/input/response.ts +++ b/packages/forms/src/patterns/input/response.ts @@ -27,9 +27,5 @@ export const parseUserInput: ParseUserInput< InputPattern, InputPatternOutput > = (pattern, obj) => { - // console.group('parseUserInput'); - // console.log(pattern); - // console.log(obj); - // console.groupEnd(); return safeZodParseToFormError(createSchema(pattern['data']), obj); }; diff --git a/packages/forms/src/patterns/repeater/index.ts b/packages/forms/src/patterns/repeater/index.ts index 568b5812..21b3b4e0 100644 --- a/packages/forms/src/patterns/repeater/index.ts +++ b/packages/forms/src/patterns/repeater/index.ts @@ -21,11 +21,6 @@ export const repeaterConfig: PatternConfig = { }, parseConfigData, parseUserInput: (pattern, input: unknown) => { - console.group('parseUserInput'); - console.log(pattern); - console.log(input); - console.groupEnd(); - // FIXME: Not sure why we're sometimes getting a string here, and sometimes // the expected object. Workaround, by accepting both. if (typeof input === 'string') { diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index f7d5e01f..eb9cf9cb 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -43,11 +43,6 @@ const parsePromptResponse = ( const pattern = getPattern(session.form, id); const patternConfig = getPatternConfig(config, pattern.type); const isValidResult = validatePattern(patternConfig, pattern, promptValue); - // console.group('parsePromptResponse'); - // console.log(pattern); - // console.log(patternConfig); - // console.log(isValidResult); - // console.groupEnd(); if (isValidResult.success) { values[patternId] = isValidResult.data; } else { diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index 36d604c8..84083c53 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -116,10 +116,6 @@ export const updateSession = ( values: PatternValueMap, errors: FormErrorMap ): FormSession => { - // console.group('updateSession'); - // console.log('values', values); - // console.log('errors', errors); - // console.groupEnd(); const keysValid = Object.keys(values).every( patternId => patternId in session.form.patterns diff --git a/packages/forms/src/util/zod.ts b/packages/forms/src/util/zod.ts index ce2d34a1..097b0437 100644 --- a/packages/forms/src/util/zod.ts +++ b/packages/forms/src/util/zod.ts @@ -33,10 +33,6 @@ export const safeZodParseFormErrors = ( schema: Schema, obj: unknown ): r.Result, FormErrors> => { - // console.group('safeZodParseFormErrors'); - // console.log(schema); - // console.log(obj); - // console.groupEnd(); const result = safeZodParse(schema, obj); if (result.success) { return r.success(result.data); From 09aa5513b0a462dee78ed3cde4673cbb69e4cdd7 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Thu, 10 Oct 2024 15:57:11 -0400 Subject: [PATCH 24/27] remove function from repeater pattern. validation occurs on individual components --- packages/forms/src/patterns/repeater/index.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/packages/forms/src/patterns/repeater/index.ts b/packages/forms/src/patterns/repeater/index.ts index 21b3b4e0..4dee4836 100644 --- a/packages/forms/src/patterns/repeater/index.ts +++ b/packages/forms/src/patterns/repeater/index.ts @@ -20,36 +20,6 @@ export const repeaterConfig: PatternConfig = { patterns: [], }, parseConfigData, - parseUserInput: (pattern, input: unknown) => { - // 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}`, - }, - }; - */ - }, getChildren(pattern, patterns) { return pattern.data.patterns.map( (patternId: string) => patterns[patternId] From 18cd7e3172ca1f4d65b36ac474331657e92e84b6 Mon Sep 17 00:00:00 2001 From: Jenny Richards <163178612+JennyRichards-Flexion@users.noreply.github.com> Date: Fri, 11 Oct 2024 08:57:22 -0700 Subject: [PATCH 25/27] Create new risk issue template (#328) New template for managing risks within GitHub, with specific fields to complete. --- .github/ISSUE_TEMPLATE/risk.md | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/risk.md diff --git a/.github/ISSUE_TEMPLATE/risk.md b/.github/ISSUE_TEMPLATE/risk.md new file mode 100644 index 00000000..5e599994 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/risk.md @@ -0,0 +1,59 @@ +--- +name: Risk +about: Document project risks +title: "[RISK]" +labels: risk +assignees: '' + +--- + +## Title +*What is the risk we need to manage?* +
    + +## Description +*Describe the potential risk. For instance: Item A cannot be completed until Item B has been purchased but approval has been delayed, or Item A requires resources that have not been identified and the project is currently resource constrained.* + +
    + + +## Category + +- [ ] Timeline +- [ ] Resource +- [ ] Environment +- [ ] Customer/Partner +- [ ] Regulatory or compliance +- [ ] Financial (cost/revenue) +- [ ] Regulatory or compliance +- [ ] Something else? Please suggest a category. + +
    + +## Potential Impact (1 - 10) +*A quantitative rating of the potential impact on the project if the risk should materialize. Impact in a Risk Register should be scored on a scale of 1 – 10 with 10 being the highest impact.* + +
    + + +## Probability (1 - 10) +*The likelihood that the risk will occur at some point in the duration of the project. This should be quantitative like Potential Impact not qualitative (high, medium or low). If you use qualitative measures you cannot calculate a Risk Score, which is done by multiplying Probability and Impact and you can easily convert a number to a descriptor e.g. 1-3 = “Low”, 4-6 = “Medium” and 7-10 = “High”.* + +
    + + +## Risk Score (Impact x Probability) + +
    + + +## Likely Outcome +*The likely consequence or impact of the risk if it materializes.* + +
    + + +## Prevention and Mitigation +*Action plan to prevent a given risk from occurring, or contingency plans if the risk occurs.* + +
    From 31e643970fde027557e947b881775982b3b25be9 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 11 Oct 2024 11:38:39 -0400 Subject: [PATCH 26/27] turn off localstorage on the repeater for now --- packages/design/src/Form/components/Repeater/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/design/src/Form/components/Repeater/index.tsx b/packages/design/src/Form/components/Repeater/index.tsx index e23ee104..4177cd8a 100644 --- a/packages/design/src/Form/components/Repeater/index.tsx +++ b/packages/design/src/Form/components/Repeater/index.tsx @@ -26,7 +26,9 @@ const Repeater: PatternComponent = props => { }); React.useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(fields.length)); + // if(!localStorage.getItem(STORAGE_KEY) && fields.length !== 1) { + // localStorage.setItem(STORAGE_KEY, JSON.stringify(fields.length)); + // } }, [fields.length]); const hasFields = React.Children.toArray(props.children).length > 0; From 15a76ef991680a0228f8238fd734f91b9ac2a374 Mon Sep 17 00:00:00 2001 From: ethangardner Date: Fri, 11 Oct 2024 12:20:42 -0400 Subject: [PATCH 27/27] unified add pattern methods to fieldset and repeaters into a single method --- .../FormEdit/components/FieldsetEdit.tsx | 8 +-- .../components/RepeaterPatternEdit.tsx | 8 +-- .../design/src/FormManager/FormEdit/store.ts | 58 ++++++++----------- packages/forms/src/builder/index.ts | 7 ++- 4 files changed, 37 insertions(+), 44 deletions(-) diff --git a/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx b/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx index 932f1f2f..5cdd681b 100644 --- a/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/FieldsetEdit.tsx @@ -37,9 +37,9 @@ const FieldsetEdit: PatternEditComponent = ({ }; const FieldsetPreview: PatternComponent = props => { - const { addPatternToFieldset, deletePattern } = useFormManagerStore( + const { addPatternToCompoundField, deletePattern } = useFormManagerStore( state => ({ - addPatternToFieldset: state.addPatternToFieldset, + addPatternToCompoundField: state.addPatternToCompoundField, deletePattern: state.deletePattern, }) ); @@ -64,7 +64,7 @@ const FieldsetPreview: PatternComponent = props => { - addPatternToFieldset(patternType, props._patternId) + addPatternToCompoundField(patternType, props._patternId) } />
    @@ -91,7 +91,7 @@ const FieldsetPreview: PatternComponent = props => { - addPatternToFieldset(patternType, props._patternId) + addPatternToCompoundField(patternType, props._patternId) } />
    diff --git a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx index 77d1bb7b..fe2b2f2f 100644 --- a/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RepeaterPatternEdit.tsx @@ -38,9 +38,9 @@ const RepeaterEdit: PatternEditComponent = ({ }; const RepeaterPreview: PatternComponent = props => { - const { addPatternToRepeater, deletePattern } = useFormManagerStore( + const { addPatternToCompoundField, deletePattern } = useFormManagerStore( state => ({ - addPatternToRepeater: state.addPatternToRepeater, + addPatternToCompoundField: state.addPatternToCompoundField, deletePattern: state.deletePattern, }) ); @@ -65,7 +65,7 @@ const RepeaterPreview: PatternComponent = props => { - addPatternToRepeater(patternType, props._patternId) + addPatternToCompoundField(patternType, props._patternId) } /> @@ -92,7 +92,7 @@ const RepeaterPreview: PatternComponent = props => { - addPatternToRepeater(patternType, props._patternId) + addPatternToCompoundField(patternType, props._patternId) } /> diff --git a/packages/design/src/FormManager/FormEdit/store.ts b/packages/design/src/FormManager/FormEdit/store.ts index e297d5f6..e1d8f6c5 100644 --- a/packages/design/src/FormManager/FormEdit/store.ts +++ b/packages/design/src/FormManager/FormEdit/store.ts @@ -25,8 +25,10 @@ export type FormEditSlice = { addPage: () => void; addPattern: (patternType: string) => void; - addPatternToFieldset: (patternType: string, targetPattern: PatternId) => void; - addPatternToRepeater: (patternType: string, targetPattern: PatternId) => void; + addPatternToCompoundField: ( + patternType: string, + targetPattern: PatternId + ) => void; clearFocus: () => void; copyPattern: (parentPatternId: PatternId, patternId: PatternId) => void; deletePattern: (id: PatternId) => void; @@ -119,44 +121,30 @@ export const createFormEditSlice = }); state.addNotification('success', 'Element copied successfully.'); }, - - addPatternToFieldset: (patternType, targetPattern) => { - const state = get(); - const builder = new BlueprintBuilder( - state.context.config, - state.session.form - ); - const newPattern = builder.addPatternToFieldset( - patternType, - targetPattern - ); - set({ - session: mergeSession(state.session, { form: builder.form }), - focus: { pattern: newPattern }, - }); - state.addNotification( - 'success', - 'Element added to fieldset successfully.' - ); - }, - addPatternToRepeater: (patternType, targetPattern) => { + addPatternToCompoundField: (patternType, targetPattern) => { const state = get(); const builder = new BlueprintBuilder( state.context.config, state.session.form ); - const newPattern = builder.addPatternToRepeater( - patternType, - targetPattern - ); - set({ - session: mergeSession(state.session, { form: builder.form }), - focus: { pattern: newPattern }, - }); - state.addNotification( - 'success', - 'Element added to repeater successfully.' - ); + const targetPatternType = builder.getPatternTypeById(targetPattern); + if (['fieldset', 'repeater'].includes(targetPatternType)) { + let newPattern: Pattern; + if (targetPatternType === 'fieldset') { + newPattern = builder.addPatternToFieldset(patternType, targetPattern); + } else { + newPattern = builder.addPatternToRepeater(patternType, targetPattern); + } + + set({ + session: mergeSession(state.session, { form: builder.form }), + focus: { pattern: newPattern }, + }); + state.addNotification( + 'success', + `Element added to ${targetPatternType} successfully.` + ); + } }, clearFocus: () => { set({ focus: undefined }); diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index e114791e..0a1fada6 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -102,6 +102,11 @@ export class BlueprintBuilder { return results.pattern; } + getPatternTypeById(patternId: PatternId) { + const root = this.form.patterns[patternId]; + return root.type; + } + addPatternToFieldset(patternType: string, fieldsetPatternId: PatternId) { const pattern = createDefaultPattern(this.config, patternType); const root = this.form.patterns[fieldsetPatternId] as FieldsetPattern; @@ -116,7 +121,7 @@ export class BlueprintBuilder { const pattern = createDefaultPattern(this.config, patternType); const root = this.form.patterns[fieldsetPatternId] as FieldsetPattern; if (root.type !== 'repeater') { - throw new Error('expected pattern to be a fieldset'); + throw new Error('expected pattern to be a repeater'); } this.bp = addPatternToRepeater(this.form, fieldsetPatternId, pattern); return pattern;