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 (
+
+ );
+};
+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 (
);
};
-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) => {
{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 (
+
+ );
+};
+
+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 ? (
-
- handleDelete(index)}
- disabled={fields.length === 1}
- >
- Delete item
-
-
- ) : null}
+
+ handleDelete(index)}
+ disabled={fields.length === 1}
+ >
+ Delete item
+
+
);
})}
- {props.showControls !== false ? (
-
-
- Add new item
-
-
- ) : null}
+
+
+ Add new item
+
+
>
) : 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 (
);
};
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 (
- );
-};
+
+
+
+ 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 (
- )}
-
- );
- };
+ )}
+
+ );
+};
- 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)}
+
+ ))}
append({})}
>
Add new item
remove(fields.length - 1)}
disabled={fields.length === 1}
>
Delete item
>
- ) : 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}
- >
- setIsOpen(!isOpen)}
- >
- {title}
-
-
- );
-};
-
-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}
- >
- setIsOpen(!isOpen)}
- >
- {' '}
-
-
- {title}
-
-
-
-
-
- );
-};
-
-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 = () => {
/>
- {
- addPage();
- }}
- >
-
-
-
-
- Page
-
-
+
@@ -85,6 +73,34 @@ export const AddPatternMenu = () => {
);
};
+const MenuItemButton = ({
+ title,
+ onClick,
+ iconPath,
+}: {
+ title: string;
+ onClick: () => void;
+ iconPath: string;
+}) => (
+
+
+
+
+
+ {title}
+
+
+);
+
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 (
<>
>
);
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 (
<>
@@ -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;