diff --git a/README.md b/README.md index da1ef07d8..5a1037774 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,23 @@ Additional documentation: - [Architectural Decision Records (ADRs)](./documents/adr/) - [Non-project contributions](./documents/value-created-log.md) +## Overview + +The platform is made up of the following high-level terms. + +### Key personas + +- Content authors: legal experts who craft guided interview experiences via a "no code" interface +- Self-represented litigants (SREs): end-users who interact with the court via guided interviews created by content authors + +### Things + +- **Blueprint**: produced by a content author, the blueprint defines the structure of an interactive session between a court and an SRL +- **Conversation**: one instance of a blueprint; the interactive session between a court and an SRL. Other terms for this concept include dialogue or session. +- **Pattern**: the building blocks of a blueprint, patterns implement UX best-practices, defining the content and behavior of the user interface. +- **Prompt**: produced by a pattern, the prompt defines what is presented to the end user at single point in a conversation. +- **Component**: user interface component that acts as the building block of prompts. + ## Development This project uses [pnpm workspaces](https://pnpm.io/workspaces). To work with this project, [install pnpm](https://pnpm.io/installation) and then the project dependencies: diff --git a/apps/doj-demo/astro.config.mjs b/apps/doj-demo/astro.config.mjs index 8562221c4..5dc1c0bfc 100644 --- a/apps/doj-demo/astro.config.mjs +++ b/apps/doj-demo/astro.config.mjs @@ -5,7 +5,7 @@ import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ output: 'server', - trailingSlash: 'never', + trailingSlash: 'always', base: addTrailingSlash(process.env.BASEURL || ''), adapter: node({ mode: 'standalone', diff --git a/apps/doj-demo/src/components/AppFormManager.tsx b/apps/doj-demo/src/components/AppFormManager.tsx index 394120a1e..32ddbb86a 100644 --- a/apps/doj-demo/src/components/AppFormManager.tsx +++ b/apps/doj-demo/src/components/AppFormManager.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { FormManager, - defaultFormElementComponents, - defaultFormElementEditComponents, + defaultPatternComponents, + defaultPatternEditComponents, } from '@atj/design'; import { getAppContext } from '../context'; @@ -14,8 +14,8 @@ export default function () { > = Record< - string, - FormElementComponent ->; +export type ComponentForPattern< + T extends PatternProps = PatternProps, +> = Record>; -export type FormElementComponent> = +export type PatternComponent> = React.ComponentType<{ pattern: T; children?: React.ReactNode; diff --git a/packages/design/src/FormManager/DocumentImporter/index.tsx b/packages/design/src/FormManager/DocumentImporter/index.tsx index 733f64e72..a969dc836 100644 --- a/packages/design/src/FormManager/DocumentImporter/index.tsx +++ b/packages/design/src/FormManager/DocumentImporter/index.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { type DocumentFieldMap, - type FormDefinition, + type Blueprint, SAMPLE_DOCUMENTS, addDocument, addDocumentFieldsToForm, @@ -24,7 +24,7 @@ const DocumentImporter = ({ baseUrl: string; formId: string; context: FormUIContext; - form: FormDefinition; + form: Blueprint; formService: FormService; }) => { const { state, actions } = useDocumentImporter(formService, form, baseUrl); @@ -184,13 +184,13 @@ const DocumentImporter = ({ type State = { page: number; - previewForm: FormDefinition; + previewForm: Blueprint; documentFields?: DocumentFieldMap; }; const useDocumentImporter = ( formService: FormService, - form: FormDefinition, + form: Blueprint, baseUrl: string ) => { const navigate = useNavigate(); @@ -203,7 +203,7 @@ const useDocumentImporter = ( data: { path: string; fields: DocumentFieldMap; - previewForm: FormDefinition; + previewForm: Blueprint; }; } | { diff --git a/packages/design/src/FormManager/FormEdit/DraggableList.tsx b/packages/design/src/FormManager/FormEdit/DraggableList.tsx index 0cadabd55..a33cbb71a 100644 --- a/packages/design/src/FormManager/FormEdit/DraggableList.tsx +++ b/packages/design/src/FormManager/FormEdit/DraggableList.tsx @@ -17,13 +17,13 @@ import { import { CSS } from '@dnd-kit/utilities'; import { - getFormElement, - type FormDefinition, - type FormElement, - FormElementId, + getPattern, + type Blueprint, + type Pattern, + PatternId, } from '@atj/forms'; -import { SequenceElement } from '@atj/forms/src/config/elements/sequence'; +import { SequencePattern } from '@atj/forms/src/patterns/sequence'; const SortableItem = ({ id, @@ -59,19 +59,19 @@ const SortableItem = ({ }; type DraggableListProps = React.PropsWithChildren<{ - element: FormElement; - form: FormDefinition; - setSelectedElement: (element: FormElement) => void; + pattern: Pattern; + form: Blueprint; + setSelectedPattern: (pattern: Pattern) => void; }>; export const DraggableList: React.FC = ({ - element, + pattern, form, - setSelectedElement, + setSelectedPattern, children, }) => { - const [elements, setElements] = useState( - element.data.elements.map((elementId: FormElementId) => { - return getFormElement(form, elementId); + const [patterns, setPatterns] = useState( + pattern.data.patterns.map((patternId: PatternId) => { + return getPattern(form, patternId); }) ); const sensors = useSensors( @@ -90,32 +90,31 @@ export const DraggableList: React.FC = ({ return; } if (active.id !== over.id) { - const oldIndex = elements.findIndex(element => { - return element.id === active.id; + const oldIndex = patterns.findIndex(pattern => { + return pattern.id === active.id; }); - const newIndex = elements.findIndex(element => { - return element.id === over.id; + const newIndex = patterns.findIndex(pattern => { + return pattern.id === over.id; }); - const newOrder = arrayMove(elements, oldIndex, newIndex); - setElements(newOrder); - setSelectedElement({ - id: element.id, - type: element.type, + const newOrder = arrayMove(patterns, oldIndex, newIndex); + setPatterns(newOrder); + setSelectedPattern({ + id: pattern.id, + type: pattern.type, data: { - elements: newOrder.map(element => element.id), + patterns: newOrder.map(pattern => pattern.id), }, - default: { - elements: [], + initial: { + patterns: [], }, - required: element.required, - } satisfies SequenceElement); + } satisfies SequencePattern); } }} > - +
    {arrayChildren.map((child, index) => ( - + {child} ))} diff --git a/packages/design/src/FormManager/FormEdit/FormElementEdit.tsx b/packages/design/src/FormManager/FormEdit/PatternEdit.tsx similarity index 63% rename from packages/design/src/FormManager/FormEdit/FormElementEdit.tsx rename to packages/design/src/FormManager/FormEdit/PatternEdit.tsx index a50155198..55e61ffff 100644 --- a/packages/design/src/FormManager/FormEdit/FormElementEdit.tsx +++ b/packages/design/src/FormManager/FormEdit/PatternEdit.tsx @@ -1,45 +1,45 @@ import React, { useEffect, useRef } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { type FormElementMap } from '@atj/forms'; +import { type PatternMap } from '@atj/forms'; import { useFormEditStore } from './store'; -export const FormElementEdit = () => { +export const PatternEdit = () => { const context = useFormEditStore(state => state.context); const form = useFormEditStore(state => state.form); - const selectedElement = useFormEditStore(state => state.selectedElement); - const { setSelectedElement, updateSelectedFormElement } = useFormEditStore( + const selectedPattern = useFormEditStore(state => state.selectedPattern); + const { setSelectedPattern, updateSelectedPattern } = useFormEditStore( state => ({ - setSelectedElement: state.setSelectedElement, - updateSelectedFormElement: state.updateSelectedFormElement, + setSelectedPattern: state.setSelectedPattern, + updateSelectedPattern: state.updateSelectedPattern, }) ); - const methods = useForm({ - defaultValues: selectedElement + const methods = useForm({ + defaultValues: selectedPattern ? { - [selectedElement.id]: selectedElement, + [selectedPattern.id]: selectedPattern, } : {}, }); const settingsContainerRef = useRef(null); useEffect(() => { - if (selectedElement === undefined) { + if (selectedPattern === undefined) { return; } methods.reset(); - methods.setValue(selectedElement.id, selectedElement); - }, [selectedElement]); + methods.setValue(selectedPattern.id, selectedPattern); + }, [selectedPattern]); // Updates the scroll position of the edit form when it's visible useEffect(() => { let frameId: number; const updatePosition = () => { if (window.innerWidth > 879) { - if (selectedElement) { + if (selectedPattern) { const element = document.querySelector( - `[data-id="${selectedElement.id}"]` + `[data-id="${selectedPattern.id}"]` ); if (element && settingsContainerRef.current) { const rect = element.getBoundingClientRect(); @@ -53,13 +53,13 @@ export const FormElementEdit = () => { return () => { cancelAnimationFrame(frameId); }; - }, [selectedElement]); + }, [selectedPattern]); - if (!selectedElement) { + if (!selectedPattern) { return; } - const SelectedEditComponent = context.editComponents[selectedElement.type]; + const SelectedEditComponent = context.editComponents[selectedPattern.type]; return (
    {
    { - updateSelectedFormElement(formData); + updateSelectedPattern(formData); })} > -

    Editing "{selectedElement.data.label}"...

    +

    Editing "{selectedPattern.data.label}"...

    setSelectedElement(undefined)} + onClick={() => setSelectedPattern(undefined)} className="usa-button close-button" type="submit" value="Cancel" diff --git a/packages/design/src/FormManager/FormEdit/Preview.tsx b/packages/design/src/FormManager/FormEdit/Preview.tsx index 3ee3c8dac..2777a1a9c 100644 --- a/packages/design/src/FormManager/FormEdit/Preview.tsx +++ b/packages/design/src/FormManager/FormEdit/Preview.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { type Pattern, createFormSession } from '@atj/forms'; +import { type PatternProps, createFormSession } from '@atj/forms'; import Form, { type ComponentForPattern, - type FormElementComponent, + type PatternComponent, type FormUIContext, } from '../../Form'; import { useFormEditStore } from './store'; @@ -64,20 +64,20 @@ const createPreviewComponents = ( /* const createSequencePatternPreviewComponent = ( - Component: FormElementComponent, + Component: PatternComponent, previewComponents: ComponentForPattern ) => { - const PatternPreviewSequenceComponent: FormElementComponent = ({ + const PatternPreviewSequenceComponent: PatternComponent = ({ pattern, }) => { - const { form, setSelectedElement } = usePreviewContext(); - const element = getFormElement(form, pattern._elementId); + const { form, setSelectedPattern } = usePreviewContext(); + const element = getPattern(form, pattern._patternId); const Component = previewComponents[pattern.type]; return ( @@ -88,24 +88,24 @@ const createSequencePatternPreviewComponent = ( */ const createPatternPreviewComponent = ( - Component: FormElementComponent, + Component: PatternComponent, uswdsRoot: string ) => { - const PatternPreviewComponent: FormElementComponent = ({ + const PatternPreviewComponent: PatternComponent = ({ pattern, }: { - pattern: Pattern; + pattern: PatternProps; }) => { - const selectedElement = useFormEditStore(state => state.selectedElement); + const selectedPattern = useFormEditStore(state => state.selectedPattern); const handleEditClick = useFormEditStore(state => state.handleEditClick); - const isSelected = selectedElement?.id === pattern._elementId; + const isSelected = selectedPattern?.id === pattern._patternId; const divClassNames = isSelected ? 'form-group-row field-selected' : 'form-group-row'; return ( -

    +
    diff --git a/packages/design/src/FormManager/FormEdit/store.tsx b/packages/design/src/FormManager/FormEdit/store.tsx index 3c1f57241..554a9c740 100644 --- a/packages/design/src/FormManager/FormEdit/store.tsx +++ b/packages/design/src/FormManager/FormEdit/store.tsx @@ -3,12 +3,12 @@ import { StoreApi, create } from 'zustand'; import { createContext } from 'zustand-utils'; import { - type FormDefinition, - type FormElementMap, - type Pattern, - getFormElement, + type Blueprint, + type PatternMap, + type PatternProps, + getPattern, FormBuilder, - FormElement, + Pattern, } from '@atj/forms'; import { type FormEditUIContext } from './types'; @@ -18,7 +18,7 @@ export const useFormEditStore = useStore; export const FormEditProvider = (props: { context: FormEditUIContext; - form: FormDefinition; + form: Blueprint; children: React.ReactNode; }) => { return ( @@ -30,12 +30,12 @@ export const FormEditProvider = (props: { type FormEditState = { context: FormEditUIContext; - form: FormDefinition; - selectedElement?: FormElement; + form: Blueprint; + selectedPattern?: Pattern; - handleEditClick: (pattern: Pattern) => void; - setSelectedElement: (element?: FormElement) => void; - updateSelectedFormElement: (formData: FormElementMap) => void; + handleEditClick: (pattern: PatternProps) => void; + setSelectedPattern: (element?: Pattern) => void; + updateSelectedPattern: (formData: PatternMap) => void; }; const createFormEditStore = ({ @@ -43,35 +43,35 @@ const createFormEditStore = ({ form, }: { context: FormEditUIContext; - form: FormDefinition; + form: Blueprint; }) => create((set, get) => ({ context, form, - handleEditClick: (pattern: Pattern) => { + handleEditClick: (pattern: PatternProps) => { const state = get(); - if (state.selectedElement?.id === pattern._elementId) { - set({ selectedElement: undefined }); + if (state.selectedPattern?.id === pattern._patternId) { + set({ selectedPattern: undefined }); } else { - const elementToSet = getFormElement(state.form, pattern._elementId); - set({ selectedElement: elementToSet }); + const elementToSet = getPattern(state.form, pattern._patternId); + set({ selectedPattern: elementToSet }); } }, - setSelectedElement: selectedElement => set({ selectedElement }), - updateSelectedFormElement: (formData: FormElementMap) => { + setSelectedPattern: selectedPattern => set({ selectedPattern }), + updateSelectedPattern: (formData: PatternMap) => { const state = get(); - if (state.selectedElement === undefined) { + if (state.selectedPattern === undefined) { console.warn('No selected element'); return; } const builder = new FormBuilder(state.form); - const success = builder.updateFormElement( + const success = builder.updatePattern( state.context.config, - state.selectedElement, + state.selectedPattern, formData ); if (success) { - set({ form: builder.form, selectedElement: undefined }); + set({ form: builder.form, selectedPattern: undefined }); } }, })); diff --git a/packages/design/src/FormManager/FormEdit/types.ts b/packages/design/src/FormManager/FormEdit/types.ts index 04d130693..afa748d64 100644 --- a/packages/design/src/FormManager/FormEdit/types.ts +++ b/packages/design/src/FormManager/FormEdit/types.ts @@ -1,24 +1,21 @@ -import { - type FormConfig, - type FormDefinition, - type FormElement, -} from '@atj/forms'; +import { type FormConfig, type Blueprint, type Pattern } from '@atj/forms'; import { type ComponentForPattern } from '../../Form'; export type FormEditUIContext = { config: FormConfig; components: ComponentForPattern; - editComponents: EditComponentForFormElement; + editComponents: EditComponentForPattern; uswdsRoot: `${string}/`; }; -export type FormElementEditComponent = - React.ComponentType<{ - context: FormEditUIContext; - form: FormDefinition; - element: T; - }>; +export type PatternEditComponent = React.ComponentType<{ + context: FormEditUIContext; + form: Blueprint; + pattern: T; +}>; -export type EditComponentForFormElement = - Record>; +export type EditComponentForPattern = Record< + string, + PatternEditComponent +>; diff --git a/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.test.ts b/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.test.ts index 5f8276f13..882a31161 100644 --- a/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.test.ts +++ b/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.test.ts @@ -21,7 +21,7 @@ describe('onFileInputChangeGetFile', () => { target: { files: [new File([], 'file-name.xml')] as unknown as FileList, }, - } as ChangeEvent); + } as ChangeEvent); }); }); }); diff --git a/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.ts b/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.ts index 4aadf56a8..7b7a641ab 100644 --- a/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.ts +++ b/packages/design/src/FormManager/FormList/PDFFileSelect/file-input.ts @@ -13,7 +13,7 @@ const readFileAsync = (file: File) => { export const onFileInputChangeGetFile = (setFile: ({ name, data }: { name: string; data: Uint8Array }) => void) => - (event: ChangeEvent) => { + (event: ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const inputFile = event.target.files[0]; readFileAsync(inputFile).then(data => { diff --git a/packages/design/src/FormManager/FormPreview/index.tsx b/packages/design/src/FormManager/FormPreview/index.tsx index a91011723..7dd933f81 100644 --- a/packages/design/src/FormManager/FormPreview/index.tsx +++ b/packages/design/src/FormManager/FormPreview/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { type FormDefinition, createFormSession } from '@atj/forms'; +import { type Blueprint, createFormSession } from '@atj/forms'; import { FormService } from '@atj/form-service'; import Form, { type FormUIContext } from '../../Form'; @@ -10,7 +10,7 @@ export default function FormPreview({ form, }: { context: FormUIContext; - form: FormDefinition; + form: Blueprint; }) { const session = createFormSession(form); return ; diff --git a/packages/design/src/config/edit/FormSummaryEdit.tsx b/packages/design/src/config/edit/FormSummaryEdit.tsx index 1092e2980..d74d7457b 100644 --- a/packages/design/src/config/edit/FormSummaryEdit.tsx +++ b/packages/design/src/config/edit/FormSummaryEdit.tsx @@ -1,13 +1,10 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { type FormSummary } from '@atj/forms/src/config/elements/form-summary'; +import { type FormSummary } from '@atj/forms/src/patterns/form-summary'; +import { PatternEditComponent } from '../../FormManager/FormEdit/types'; -import { FormElementEditComponent } from '..'; - -const FormSummaryEdit: FormElementEditComponent = ({ - element, -}) => { +const FormSummaryEdit: PatternEditComponent = ({ pattern }) => { const { register } = useFormContext(); return (
    @@ -16,7 +13,7 @@ const FormSummaryEdit: FormElementEditComponent = ({ Title @@ -26,7 +23,7 @@ const FormSummaryEdit: FormElementEditComponent = ({ Description
    diff --git a/packages/design/src/config/edit/InputElementEdit.tsx b/packages/design/src/config/edit/InputPatternEdit.tsx similarity index 55% rename from packages/design/src/config/edit/InputElementEdit.tsx rename to packages/design/src/config/edit/InputPatternEdit.tsx index 3e4f0fe1f..ce2082448 100644 --- a/packages/design/src/config/edit/InputElementEdit.tsx +++ b/packages/design/src/config/edit/InputPatternEdit.tsx @@ -1,58 +1,55 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { type InputElement } from '@atj/forms/src/config/elements/input'; +import { type InputPattern } from '@atj/forms/src/patterns/input'; +import { PatternEditComponent } from '../../FormManager/FormEdit/types'; -import { FormElementEditComponent } from '..'; - -const InputElementEdit: FormElementEditComponent = ({ - element, -}) => { +const InputPatternEdit: PatternEditComponent = ({ pattern }) => { const { register } = useFormContext(); return (
    -
    -
    -
    - @@ -23,7 +24,7 @@ const ParagraphElementEdit: FormElementComponent = ({
    @@ -67,15 +67,15 @@ const SortableItem = ({ id, form, element, context }: ItemProps) => { ); }; -const SequenceElementEdit: FormElementEditComponent = ({ +const SequencePatternEdit: PatternEditComponent = ({ context, form, - element, + pattern, }) => { const { register, setValue } = useFormContext(); - const [elements, setElements] = useState( - element.data.elements.map(elementId => { - return form.elements[elementId]; + const [patterns, setPatterns] = useState( + pattern.data.patterns.map((patternId: string) => { + return form.patterns[patternId]; }) ); const sensors = useSensors( @@ -94,38 +94,37 @@ const SequenceElementEdit: FormElementEditComponent = ({ return; } if (active.id !== over.id) { - const oldIndex = elements.findIndex(element => { - return element.id === active.id; + const oldIndex = patterns.findIndex(pattern => { + return pattern.id === active.id; }); - const newIndex = elements.findIndex(element => { - return element.id === over.id; + const newIndex = patterns.findIndex(pattern => { + return pattern.id === over.id; }); - const newOrder = arrayMove(elements, oldIndex, newIndex); - setElements(newOrder); - setValue(element.id, { - id: element.id, - type: element.type, + const newOrder = arrayMove(patterns, oldIndex, newIndex); + setPatterns(newOrder); + setValue(pattern.id, { + ...pattern, data: { - elements: newOrder.map(element => element.id), + patterns: newOrder.map(pattern => pattern.id), }, - } satisfies SequenceElement); + } satisfies SequencePattern); } }} >
      - - - - {elements.map(elements => ( + + + + {patterns.map(patterns => ( ))} @@ -136,4 +135,4 @@ const SequenceElementEdit: FormElementEditComponent = ({ ); }; -export default SequenceElementEdit; +export default SequencePatternEdit; diff --git a/packages/design/src/config/edit/SubmissionConfirmationEdit.tsx b/packages/design/src/config/edit/SubmissionConfirmationEdit.tsx index db5ec764c..0c4b28d9e 100644 --- a/packages/design/src/config/edit/SubmissionConfirmationEdit.tsx +++ b/packages/design/src/config/edit/SubmissionConfirmationEdit.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { type InputElement } from '@atj/forms/src/config/elements/input'; +import { type InputPattern } from '@atj/forms/src/patterns/input'; +import { type PatternEditComponent } from '../../FormManager/FormEdit/types'; -import { FormElementEditComponent } from '..'; - -const InputElementEdit: FormElementEditComponent = ({ - element, +const SubmissionConfirmationEdit: PatternEditComponent = ({ + pattern, }) => { const { register } = useFormContext(); return ( @@ -16,7 +15,7 @@ const InputElementEdit: FormElementEditComponent = ({ Field label @@ -27,7 +26,7 @@ const InputElementEdit: FormElementEditComponent = ({
    @@ -37,14 +36,14 @@ const InputElementEdit: FormElementEditComponent = ({
    @@ -54,12 +53,12 @@ const InputElementEdit: FormElementEditComponent = ({ @@ -69,4 +68,4 @@ const InputElementEdit: FormElementEditComponent = ({ ); }; -export default InputElementEdit; +export default SubmissionConfirmationEdit; diff --git a/packages/design/src/config/edit/index.ts b/packages/design/src/config/edit/index.ts index cf4e2dd93..8813fc0fd 100644 --- a/packages/design/src/config/edit/index.ts +++ b/packages/design/src/config/edit/index.ts @@ -1,10 +1,10 @@ -import InputElementEdit from './InputElementEdit'; -import SequenceElementEdit from './SequenceElementEdit'; +import InputPatternEdit from './InputPatternEdit'; +import SequencePatternEdit from './SequencePatternEdit'; import SubmissionConfirmationEdit from './SubmissionConfirmationEdit'; -import { type EditComponentForFormElement } from '../../FormManager/FormEdit/types'; +import { type EditComponentForPattern } from '../../FormManager/FormEdit/types'; -export const defaultFormElementEditComponents: EditComponentForFormElement = { - input: InputElementEdit, - sequence: SequenceElementEdit, +export const defaultPatternEditComponents: EditComponentForPattern = { + input: InputPatternEdit, + sequence: SequencePatternEdit, 'submission-confirmation': SubmissionConfirmationEdit, }; diff --git a/packages/design/src/config/view/Fieldset/index.tsx b/packages/design/src/config/view/Fieldset/index.tsx index d2b23dd5c..2e09f7c14 100644 --- a/packages/design/src/config/view/Fieldset/index.tsx +++ b/packages/design/src/config/view/Fieldset/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { type FieldsetPattern, type Pattern } from '@atj/forms'; +import { type FieldsetProps } from '@atj/forms'; -import { type FormElementComponent } from '../../../Form'; +import { type PatternComponent } from '../../../Form'; -const FormSummary: FormElementComponent> = ({ +const FormSummary: PatternComponent = ({ pattern, children, }) => { diff --git a/packages/design/src/config/view/FormSummary/FormSummary.stories.tsx b/packages/design/src/config/view/FormSummary/FormSummary.stories.tsx index b0c63e526..b77317ca9 100644 --- a/packages/design/src/config/view/FormSummary/FormSummary.stories.tsx +++ b/packages/design/src/config/view/FormSummary/FormSummary.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import FormSummary from '.'; +import { type FormSummaryProps } from '@atj/forms'; export default { title: 'patterns/FormSummary', @@ -11,22 +12,22 @@ export default { export const FormSummaryWithLongDescription = { args: { pattern: { - _elementId: 'test-id', + _patternId: 'test-id', type: 'form-summary', title: 'Form title', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - }, + } as FormSummaryProps, }, } satisfies StoryObj; export const FormSummaryWithShortDescription = { args: { pattern: { - _elementId: 'test-id', + _patternId: 'test-id', type: 'form-summary', title: 'Title 2', description: 'Short description', - }, + } as FormSummaryProps, }, } satisfies StoryObj; diff --git a/packages/design/src/config/view/FormSummary/index.tsx b/packages/design/src/config/view/FormSummary/index.tsx index 95a7002e5..0d77b53ad 100644 --- a/packages/design/src/config/view/FormSummary/index.tsx +++ b/packages/design/src/config/view/FormSummary/index.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { Pattern, type FormSummaryPattern } from '@atj/forms'; -import { type FormElementComponent } from '../../../Form'; +import { type FormSummaryProps } from '@atj/forms'; +import { type PatternComponent } from '../../../Form'; -const FormSummary: FormElementComponent> = ({ - pattern, -}) => { +const FormSummary: PatternComponent = ({ pattern }) => { return ( <>
    diff --git a/packages/design/src/config/view/Paragraph/index.tsx b/packages/design/src/config/view/Paragraph/index.tsx index bf9428a0c..ef01808fb 100644 --- a/packages/design/src/config/view/Paragraph/index.tsx +++ b/packages/design/src/config/view/Paragraph/index.tsx @@ -1,12 +1,10 @@ import React from 'react'; -import { type ParagraphPattern, type Pattern } from '@atj/forms'; +import { type ParagraphProps } from '@atj/forms'; -import { type FormElementComponent } from '../../../Form'; +import { type PatternComponent } from '../../../Form'; -const FormSummary: FormElementComponent> = ({ - pattern, -}) => { +const FormSummary: PatternComponent = ({ pattern }) => { if (pattern.style === 'heading') { return ( <> diff --git a/packages/design/src/config/view/Sequence/index.tsx b/packages/design/src/config/view/Sequence/index.tsx index 92e92d75d..955b4765f 100644 --- a/packages/design/src/config/view/Sequence/index.tsx +++ b/packages/design/src/config/view/Sequence/index.tsx @@ -1,13 +1,8 @@ import React from 'react'; -import { type Pattern } from '@atj/forms'; -import { SequenceElement } from '@atj/forms/src/config/elements/sequence'; +import { type PatternComponent } from '../../../Form'; -import { FormElementComponent } from '../../../Form'; - -const Sequence: FormElementComponent> = ({ - children, -}) => { +const Sequence: PatternComponent = ({ children }) => { return <>{children}; }; diff --git a/packages/design/src/config/view/SubmissionConfirmation/SubmissionConfirmation.stories.tsx b/packages/design/src/config/view/SubmissionConfirmation/SubmissionConfirmation.stories.tsx index b745e3d6a..fea444e56 100644 --- a/packages/design/src/config/view/SubmissionConfirmation/SubmissionConfirmation.stories.tsx +++ b/packages/design/src/config/view/SubmissionConfirmation/SubmissionConfirmation.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import SubmissionConfirmation, { type SubmissionConfirmationProps } from '.'; +import SubmissionConfirmation from '.'; +import { type SubmissionConfirmationProps } from '@atj/forms'; export default { title: 'patterns/SubmissionConfirmation', @@ -10,7 +11,7 @@ export default { export const SubmissionConfirmationExample = { args: { - prompt: { + pattern: { type: 'submission-confirmation', table: [ { label: 'Field 1', value: 'Value 1' }, @@ -18,6 +19,6 @@ export const SubmissionConfirmationExample = { { label: 'Field 3', value: 'Value 3' }, { label: 'Field 4', value: 'Value 4' }, ], - }, - } satisfies SubmissionConfirmationProps, + } as SubmissionConfirmationProps, + }, } satisfies StoryObj; diff --git a/packages/design/src/config/view/SubmissionConfirmation/index.tsx b/packages/design/src/config/view/SubmissionConfirmation/index.tsx index b9cd3294c..d763ecb11 100644 --- a/packages/design/src/config/view/SubmissionConfirmation/index.tsx +++ b/packages/design/src/config/view/SubmissionConfirmation/index.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Pattern, type SubmissionConfirmationPattern } from '@atj/forms'; -import { FormElementComponent } from '../../../Form'; +import { type SubmissionConfirmationProps } from '@atj/forms'; +import { type PatternComponent } from '../../../Form'; -const SubmissionConfirmation: FormElementComponent< - Pattern -> = ({ pattern }) => { +const SubmissionConfirmation: PatternComponent = ({ + pattern, +}) => { return ( <> diff --git a/packages/design/src/config/view/TextInput/TestInput.stories.tsx b/packages/design/src/config/view/TextInput/TestInput.stories.tsx index dd1b139b7..711386809 100644 --- a/packages/design/src/config/view/TextInput/TestInput.stories.tsx +++ b/packages/design/src/config/view/TextInput/TestInput.stories.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import type { Meta, StoryObj } from '@storybook/react'; -import { type Pattern, type TextInputPattern } from '@atj/forms'; +import { type TextInputProps } from '@atj/forms'; import TextInput from '.'; export default { @@ -27,25 +27,25 @@ export default { export const Required = { args: { pattern: { - _elementId: '', + _patternId: '', type: 'input', inputId: 'test-prompt', value: '', label: 'Please enter your first name.', required: true, - } as Pattern, + } as TextInputProps, }, } satisfies StoryObj; export const NotRequired = { args: { pattern: { - _elementId: '', + _patternId: '', type: 'input', inputId: 'test-prompt', value: '', label: 'Please enter your first name.', required: false, - } as Pattern, + } as TextInputProps, }, } satisfies StoryObj; diff --git a/packages/design/src/config/view/TextInput/index.tsx b/packages/design/src/config/view/TextInput/index.tsx index 1b618d0ae..3ea577038 100644 --- a/packages/design/src/config/view/TextInput/index.tsx +++ b/packages/design/src/config/view/TextInput/index.tsx @@ -2,12 +2,10 @@ import classNames from 'classnames'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { Pattern, type TextInputPattern } from '@atj/forms'; -import { type FormElementComponent } from '../../../Form'; +import { type TextInputProps } from '@atj/forms'; +import { type PatternComponent } from '../../../Form'; -const TextInput: FormElementComponent> = ({ - pattern, -}) => { +const TextInput: PatternComponent = ({ pattern }) => { const { register } = useFormContext(); return (
    diff --git a/packages/design/src/config/view/index.tsx b/packages/design/src/config/view/index.tsx index 1e704d5e6..0066eb4e9 100644 --- a/packages/design/src/config/view/index.tsx +++ b/packages/design/src/config/view/index.tsx @@ -6,7 +6,7 @@ import SubmissionConfirmation from './SubmissionConfirmation'; import TextInput from './TextInput'; import { type ComponentForPattern } from '../../Form'; -export const defaultFormElementComponents: ComponentForPattern = { +export const defaultPatternComponents: ComponentForPattern = { fieldset: Fieldset, 'form-summary': FormSummary, input: TextInput, diff --git a/packages/design/src/test-form.ts b/packages/design/src/test-form.ts index 4caaf04da..4a77f1934 100644 --- a/packages/design/src/test-form.ts +++ b/packages/design/src/test-form.ts @@ -1,11 +1,13 @@ import { createForm, createFormSession, defaultFormConfig } from '@atj/forms'; import { - defaultFormElementComponents, - defaultFormElementEditComponents, + defaultPatternComponents, + defaultPatternEditComponents, } from './config'; import { FormUIContext } from 'Form'; import { type FormEditUIContext } from './FormManager/FormEdit/types'; +import { SequencePattern } from '@atj/forms/src/patterns/sequence'; +import { InputPattern } from '@atj/forms/src/patterns/input'; export const createTestForm = () => { return createForm( @@ -15,40 +17,49 @@ export const createTestForm = () => { }, { root: 'root', - elements: [ + patterns: [ { type: 'sequence', id: 'root', data: { - elements: ['element-1', 'element-2'], + patterns: ['element-1', 'element-2'], }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, - }, + } as SequencePattern, { type: 'input', id: 'element-1', data: { - text: 'FormElement 1', + label: 'Pattern 1', + initial: '', required: true, + maxLength: 128, + }, + initial: { + label: 'Pattern 1', initial: '', + required: true, + maxLength: 128, }, - default: '', - required: true, - }, + } as InputPattern, { type: 'input', id: 'element-2', data: { - text: 'FormElement 2', - required: false, + label: 'Pattern 2', initial: 'test', + required: true, + maxLength: 128, + }, + initial: { + label: 'Pattern 2', + initial: 'test', + required: true, + maxLength: 128, }, - default: '', - required: true, - }, + } as InputPattern, ], } ); @@ -58,14 +69,14 @@ export const createTestFormConfig = () => { return defaultFormConfig; }; -export const createTestFormElementComponentMap = () => { - return defaultFormElementComponents; +export const createTestPatternComponentMap = () => { + return defaultPatternComponents; }; export const createTestFormContext = (): FormUIContext => { return { config: defaultFormConfig, - components: defaultFormElementComponents, + components: defaultPatternComponents, uswdsRoot: '/uswds/', }; }; @@ -73,8 +84,8 @@ export const createTestFormContext = (): FormUIContext => { export const createTestFormEditContext = (): FormEditUIContext => { return { config: defaultFormConfig, - components: defaultFormElementComponents, - editComponents: defaultFormElementEditComponents, + components: defaultPatternComponents, + editComponents: defaultPatternEditComponents, uswdsRoot: `/static/uswds/`, }; }; diff --git a/packages/form-service/src/context/browser/form-repo.ts b/packages/form-service/src/context/browser/form-repo.ts index f2e13978a..ba2ccd100 100644 --- a/packages/form-service/src/context/browser/form-repo.ts +++ b/packages/form-service/src/context/browser/form-repo.ts @@ -1,10 +1,10 @@ import { Result } from '@atj/common'; -import { type FormDefinition } from '@atj/forms'; +import { type Blueprint } from '@atj/forms'; export const getFormFromStorage = ( storage: Storage, id?: string -): FormDefinition | null => { +): Blueprint | null => { if (!storage || !id) { return null; } @@ -33,7 +33,7 @@ export const getFormSummaryListFromStorage = (storage: Storage) => { return null; } return forms.map(key => { - const form = getFormFromStorage(storage, key) as FormDefinition; + const form = getFormFromStorage(storage, key) as Blueprint; if (form === null) { throw new Error('key mismatch'); } @@ -47,7 +47,7 @@ export const getFormSummaryListFromStorage = (storage: Storage) => { export const addFormToStorage = ( storage: Storage, - form: FormDefinition + form: Blueprint ): Result => { const uuid = crypto.randomUUID(); @@ -65,7 +65,7 @@ export const addFormToStorage = ( export const saveFormToStorage = ( storage: Storage, formId: string, - form: FormDefinition + form: Blueprint ) => { try { storage.setItem(formId, stringifyForm(form)); @@ -84,7 +84,7 @@ export const deleteFormFromStorage = (storage: Storage, formId: string) => { storage.removeItem(formId); }; -const stringifyForm = (form: FormDefinition) => { +const stringifyForm = (form: Blueprint) => { return JSON.stringify({ ...form, outputs: form.outputs.map(output => ({ @@ -95,8 +95,8 @@ const stringifyForm = (form: FormDefinition) => { }); }; -const parseStringForm = (formString: string): FormDefinition => { - const form = JSON.parse(formString) as FormDefinition; +const parseStringForm = (formString: string): Blueprint => { + const form = JSON.parse(formString) as Blueprint; return { ...form, outputs: form.outputs.map(output => ({ diff --git a/packages/form-service/src/context/test/storage.ts b/packages/form-service/src/context/test/storage.ts index bcc9831e5..3de3dfbca 100644 --- a/packages/form-service/src/context/test/storage.ts +++ b/packages/form-service/src/context/test/storage.ts @@ -1,7 +1,7 @@ -import { type FormDefinition } from '@atj/forms'; +import { type Blueprint } from '@atj/forms'; import { saveFormToStorage } from '../browser/form-repo'; -export type TestData = Record; +export type TestData = Record; export const createTestStorage = (testData: TestData): Storage => { const records: Record = {}; diff --git a/packages/form-service/src/operations/add-form.ts b/packages/form-service/src/operations/add-form.ts index 997613c9d..079300dd2 100644 --- a/packages/form-service/src/operations/add-form.ts +++ b/packages/form-service/src/operations/add-form.ts @@ -1,11 +1,11 @@ import { Result } from '@atj/common'; -import { FormDefinition } from '@atj/forms'; +import { Blueprint } from '@atj/forms'; import { addFormToStorage } from '../context/browser/form-repo'; export const addForm = ( ctx: { storage: Storage }, - form: FormDefinition + form: Blueprint ): Result => { return addFormToStorage(ctx.storage, form); }; diff --git a/packages/form-service/src/operations/get-form.ts b/packages/form-service/src/operations/get-form.ts index 4c5c94b41..077a351b3 100644 --- a/packages/form-service/src/operations/get-form.ts +++ b/packages/form-service/src/operations/get-form.ts @@ -1,12 +1,12 @@ import { Result } from '@atj/common'; -import { type FormDefinition } from '@atj/forms'; +import { type Blueprint } from '@atj/forms'; import { getFormFromStorage } from '../context/browser/form-repo'; export const getForm = ( ctx: { storage: Storage }, formId: string -): Result => { +): Result => { const result = getFormFromStorage(ctx.storage, formId); if (result === null) { return { diff --git a/packages/form-service/src/operations/save-form.ts b/packages/form-service/src/operations/save-form.ts index 7b537644f..d689b1dfb 100644 --- a/packages/form-service/src/operations/save-form.ts +++ b/packages/form-service/src/operations/save-form.ts @@ -1,12 +1,12 @@ import { VoidResult } from '@atj/common'; -import { FormDefinition } from '@atj/forms'; +import { Blueprint } from '@atj/forms'; import { saveFormToStorage } from '../context/browser/form-repo'; export const saveForm = ( ctx: { storage: Storage }, formId: string, - form: FormDefinition + form: Blueprint ): VoidResult => { const result = saveFormToStorage(ctx.storage, formId, form); if (result.success === false) { diff --git a/packages/form-service/src/operations/submit-form.ts b/packages/form-service/src/operations/submit-form.ts index 5c9fe47da..e0d46e167 100644 --- a/packages/form-service/src/operations/submit-form.ts +++ b/packages/form-service/src/operations/submit-form.ts @@ -1,7 +1,7 @@ import { type Result } from '@atj/common'; import { type FormConfig, - type FormDefinition, + type Blueprint, type FormSession, applyPromptResponse, createFormOutputFieldData, @@ -55,7 +55,7 @@ export const submitForm = async ( }; const generateDocumentPackage = async ( - form: FormDefinition, + form: Blueprint, formData: Record ) => { const errors = new Array(); diff --git a/packages/form-service/src/types.ts b/packages/form-service/src/types.ts index fc65989fb..8cc51342b 100644 --- a/packages/form-service/src/types.ts +++ b/packages/form-service/src/types.ts @@ -1,14 +1,14 @@ import { Result, VoidResult } from '@atj/common'; -import { FormDefinition, FormSession } from '@atj/forms'; +import { Blueprint, FormSession } from '@atj/forms'; import { FormListItem } from './operations/get-form-list'; export type FormService = { - addForm: (form: FormDefinition) => Result; + addForm: (form: Blueprint) => Result; deleteForm: (formId: string) => VoidResult; - getForm: (formId: string) => Result; + getForm: (formId: string) => Result; getFormList: () => Result; - saveForm: (formId: string, form: FormDefinition) => VoidResult; + saveForm: (formId: string, form: Blueprint) => VoidResult; submitForm: ( //sessionId: string, session: FormSession, // TODO: load session from storage by ID diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index 7fa4116b6..5ae55e78d 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -1,23 +1,23 @@ import { - type FormDefinition, + type Blueprint, + type FormConfig, type FormSummary, + type Pattern, + type PatternMap, addDocument, - nullFormDefinition, + nullBlueprint, updateFormSummary, - updateFormElement, - FormElementMap, - FormElement, - FormConfig, + updatePatternFromFormData, } from '..'; export class FormBuilder { - private _form: FormDefinition; + private _form: Blueprint; - constructor(initialForm: FormDefinition = nullFormDefinition) { - this._form = initialForm || nullFormDefinition; + constructor(initialForm: Blueprint = nullBlueprint) { + this._form = initialForm || nullBlueprint; } - get form(): FormDefinition { + get form(): Blueprint { return this._form; } @@ -30,15 +30,11 @@ export class FormBuilder { this._form = updatedForm; } - updateFormElement( - config: FormConfig, - formElement: FormElement, - formData: FormElementMap - ) { - const updatedElement = updateFormElement( + updatePattern(config: FormConfig, pattern: Pattern, formData: PatternMap) { + const updatedElement = updatePatternFromFormData( config, this.form, - formElement, + pattern, formData ); if (!updatedElement) { diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts new file mode 100644 index 000000000..51f79572c --- /dev/null +++ b/packages/forms/src/components.ts @@ -0,0 +1,157 @@ +import { getRootPattern } from '..'; +import { + type FormConfig, + type Pattern, + type PatternId, + getPatternConfig, +} from './pattern'; +import { type FormSession, nullSession, sessionIsComplete } from './session'; + +export type TextInputProps = PatternProps<{ + type: 'input'; + inputId: string; + value: string; + label: string; + required: boolean; + error?: string; +}>; + +export type FormSummaryProps = PatternProps<{ + type: 'form-summary'; + title: string; + description: string; +}>; + +export type SubmissionConfirmationProps = PatternProps<{ + type: 'submission-confirmation'; + table: { label: string; value: string }[]; +}>; + +export type ParagraphProps = PatternProps<{ + type: 'paragraph'; + text: string; + style: 'indent' | 'normal' | 'heading' | 'subheading'; +}>; + +export type FieldsetProps = PatternProps<{ + type: 'fieldset'; + legend: string; +}>; + +export type PatternProps = { + _patternId: PatternId; + _children: PromptPart[]; + type: string; +} & T; + +export type SubmitAction = { + type: 'submit'; + text: 'Submit'; +}; +export type PromptAction = SubmitAction; + +export type PromptPart = { + pattern: PatternProps; + children: PromptPart[]; +}; + +export type Prompt = { + actions: PromptAction[]; + parts: PromptPart[]; +}; + +export const createPrompt = ( + config: FormConfig, + session: FormSession, + options: { validate: boolean } +): Prompt => { + if (options.validate && sessionIsComplete(config, session)) { + return { + actions: [], + parts: [ + { + pattern: { + _patternId: 'submission-confirmation', + type: 'submission-confirmation', + table: Object.entries(session.data.values) + .filter(([patternId, value]) => { + const elemConfig = getPatternConfig( + config, + session.form.patterns[patternId].type + ); + return elemConfig.acceptsInput; + }) + .map(([patternId, value]) => { + return { + label: session.form.patterns[patternId].data.label, + value: value, + }; + }), + } as SubmissionConfirmationProps, + children: [], + }, + ], + }; + } + const parts: PromptPart[] = [ + { + pattern: { + _patternId: 'form-summary', + type: 'form-summary', + title: session.form.summary.title, + description: session.form.summary.description, + } as FormSummaryProps, + children: [], + }, + ]; + const root = getRootPattern(session.form); + parts.push(createPromptForPattern(config, session, root, options)); + return { + actions: [ + { + type: 'submit', + text: 'Submit', + }, + ], + parts, + }; +}; + +export type CreatePrompt = ( + config: FormConfig, + session: FormSession, + pattern: T, + options: { validate: boolean } +) => PromptPart; + +export const createPromptForPattern: CreatePrompt = ( + config, + session, + pattern, + options +) => { + const patternConfig = getPatternConfig(config, pattern.type); + return patternConfig.createPrompt(config, session, pattern, options); +}; + +export const isPromptAction = (prompt: Prompt, action: string) => { + return prompt.actions.find(a => a.type === action); +}; + +export const createNullPrompt = ({ + config, + pattern, +}: { + config: FormConfig; + pattern: Pattern; +}): Prompt => { + const formPatternConfig = getPatternConfig(config, pattern.type); + return { + parts: [ + formPatternConfig.createPrompt(config, nullSession, pattern, { + validate: false, + }), + ], + actions: [], + }; +}; diff --git a/packages/forms/src/config.ts b/packages/forms/src/config.ts new file mode 100644 index 000000000..f77b1adcd --- /dev/null +++ b/packages/forms/src/config.ts @@ -0,0 +1 @@ +export { defaultFormConfig } from './patterns'; diff --git a/packages/forms/src/config/elements/fieldset.ts b/packages/forms/src/config/elements/fieldset.ts deleted file mode 100644 index 6fa993abc..000000000 --- a/packages/forms/src/config/elements/fieldset.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as z from 'zod'; - -import { type FormElementConfig } from '..'; -import { type FormElement, type FormElementId } from '../../element'; -import { - type FieldsetPattern, - type Pattern, - createPromptForElement, -} from '../../pattern'; -import { safeZodParse } from '../../util/zod'; -import { getFormElement } from '../..'; - -export type FieldsetElement = FormElement<{ - legend?: string; - elements: FormElementId[]; -}>; - -const FieldsetSchema = z.array(z.string()); - -const configSchema = z.object({ - legend: z.string().optional(), - elements: z.array(z.string()), -}); - -export const fieldsetConfig: FormElementConfig = { - acceptsInput: false, - initial: { - elements: [], - }, - parseData: (_, obj) => { - return safeZodParse(FieldsetSchema, obj); - }, - parseConfigData: obj => safeZodParse(configSchema, obj), - getChildren(element, elements) { - return element.data.elements.map( - (elementId: string) => elements[elementId] - ); - }, - createPrompt(config, session, element, options) { - const children = element.data.elements.map((elementId: string) => { - const element = getFormElement(session.form, elementId); - return createPromptForElement(config, session, element, options); - }); - return { - pattern: { - _children: children, - _elementId: element.id, - type: 'fieldset', - legend: element.data.legend, - } satisfies Pattern, - children, - }; - }, -}; diff --git a/packages/forms/src/config/elements/form-summary.ts b/packages/forms/src/config/elements/form-summary.ts deleted file mode 100644 index c11811890..000000000 --- a/packages/forms/src/config/elements/form-summary.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as z from 'zod'; - -import { type FormElementConfig } from '..'; -import { type FormElement } from '../../element'; -import { FormSummaryPattern, type Pattern } from '../../pattern'; -import { safeZodParse } from '../../util/zod'; - -const configSchema = z.object({ - title: z.string().max(128), - summary: z.string().max(2024), -}); -export type FormSummary = FormElement>; - -export const formSummaryConfig: FormElementConfig = { - acceptsInput: false, - initial: { - text: '', - initial: '', - required: true, - maxLength: 128, - }, - parseData: obj => safeZodParse(configSchema, obj), // make this optional? - parseConfigData: obj => safeZodParse(configSchema, obj), - getChildren() { - return []; - }, - createPrompt(_, session, element, options) { - return { - pattern: { - _elementId: element.id, - type: 'form-summary', - title: element.data.title, - description: element.data.description, - } as Pattern, - children: [], - }; - }, -}; diff --git a/packages/forms/src/config/elements/input.ts b/packages/forms/src/config/elements/input.ts deleted file mode 100644 index afe2c8a83..000000000 --- a/packages/forms/src/config/elements/input.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as z from 'zod'; - -import { type FormElementConfig } from '..'; -import { type FormElement, validateElement } from '../../element'; -import { type Pattern, type TextInputPattern } from '../../pattern'; -import { getFormSessionValue } from '../../session'; -import { safeZodParse } from '../../util/zod'; - -const configSchema = z.object({ - label: z.string(), - initial: z.string(), - required: z.boolean(), - maxLength: z.coerce.number(), -}); -export type InputElement = FormElement>; - -const createSchema = (data: InputElement['data']) => - z.string().max(data.maxLength); - -export const inputConfig: FormElementConfig = { - acceptsInput: true, - initial: { - label: '', - initial: '', - required: true, - maxLength: 128, - }, - parseData: (elementData, obj) => safeZodParse(createSchema(elementData), obj), - parseConfigData: obj => safeZodParse(configSchema, obj), - getChildren() { - return []; - }, - createPrompt(_, session, element, options) { - const extraAttributes: Record = {}; - const sessionValue = getFormSessionValue(session, element.id); - if (options.validate) { - const isValidResult = validateElement(inputConfig, element, sessionValue); - if (!isValidResult.success) { - extraAttributes['error'] = isValidResult.error; - } - } - return { - pattern: { - _elementId: element.id, - type: 'input', - inputId: element.id, - value: sessionValue, - label: element.data.label, - required: element.data.required, - ...extraAttributes, - } as Pattern, - children: [], - }; - }, -}; diff --git a/packages/forms/src/config/elements/paragraph.ts b/packages/forms/src/config/elements/paragraph.ts deleted file mode 100644 index 2e34c92ca..000000000 --- a/packages/forms/src/config/elements/paragraph.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as z from 'zod'; - -import { type FormElementConfig } from '..'; -import { type FormElement, validateElement } from '../../element'; -import { type Pattern, type ParagraphPattern } from '../../pattern'; -import { getFormSessionValue } from '../../session'; -import { safeZodParse } from '../../util/zod'; - -const configSchema = z.object({ - text: z.string(), - maxLength: z.coerce.number(), -}); -export type ParagraphElement = FormElement>; - -const createSchema = (data: ParagraphElement['data']) => - z.string().max(data.maxLength); - -export const paragraphConfig: FormElementConfig = { - acceptsInput: false, - initial: { - text: 'normal', - maxLength: 2048, - }, - parseData: (elementData, obj) => safeZodParse(createSchema(elementData), obj), - parseConfigData: obj => safeZodParse(configSchema, obj), - getChildren() { - return []; - }, - createPrompt(_, session, element, options) { - return { - pattern: { - _elementId: element.id, - type: 'paragraph' as const, - text: element.data.text, - style: element.data.style, - } as Pattern, - children: [], - }; - }, -}; diff --git a/packages/forms/src/config/elements/sequence.ts b/packages/forms/src/config/elements/sequence.ts deleted file mode 100644 index 6d4624d18..000000000 --- a/packages/forms/src/config/elements/sequence.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as z from 'zod'; - -import { type FormElementConfig } from '..'; -import { type FormElement, type FormElementId } from '../../element'; -import { createPromptForElement } from '../../pattern'; -import { safeZodParse } from '../../util/zod'; -import { getFormElement } from '../..'; - -export type SequenceElement = FormElement<{ - elements: FormElementId[]; -}>; - -const sequenceSchema = z.array(z.string()); - -const configSchema = z.object({ - elements: z.array(z.string()), -}); - -export const sequenceConfig: FormElementConfig = { - acceptsInput: false, - initial: { - elements: [], - }, - parseData: (_, obj) => { - return safeZodParse(sequenceSchema, obj); - }, - parseConfigData: obj => safeZodParse(configSchema, obj), - getChildren(element, elements) { - return element.data.elements.map( - (elementId: string) => elements[elementId] - ); - }, - createPrompt(config, session, element, options) { - const children = element.data.elements.map((elementId: string) => { - const childElement = getFormElement(session.form, elementId); - return createPromptForElement(config, session, childElement, options); - }); - return { - pattern: { - _children: children, - _elementId: element.id, - type: 'sequence', - }, - children, - }; - }, -}; diff --git a/packages/forms/src/config/index.ts b/packages/forms/src/config/index.ts deleted file mode 100644 index fbb57c458..000000000 --- a/packages/forms/src/config/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - type FormElement, - type FormElementId, - type ParseFormElementData, - type ParseFormElementConfigData, -} from '../element'; -import { type CreatePrompt } from '../pattern'; - -export { defaultFormConfig } from './config'; - -export type FormElementConfig = { - acceptsInput: boolean; - initial: ThisFormElement['data']; - parseData: ParseFormElementData; - parseConfigData: ParseFormElementConfigData; - getChildren: ( - element: ThisFormElement, - elements: Record - ) => FormElement[]; - createPrompt: CreatePrompt; -}; -export type FormConfig = { - elements: Record>; -}; - -export type ConfigElements = ReturnType< - Config['elements'][keyof Config['elements']]['parseData'] ->; diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index f5b3bb8ac..dbea7c420 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -1,34 +1,34 @@ import { - FormDefinition, - FormElement, + Blueprint, + Pattern, addFormOutput, - addFormElements, - addFormElementMap, + addPatterns, + addPatternMap, updateFormSummary, } from '..'; -import { InputElement } from '../config/elements/input'; +import { InputPattern } from '../patterns/input'; import { PDFDocument, getDocumentFieldData } from './pdf'; -import { getSuggestedFormElementsFromCache } from './suggestions'; +import { getSuggestedPatternsFromCache } from './suggestions'; import { DocumentFieldMap } from './types'; export type DocumentTemplate = PDFDocument; export const addDocument = async ( - form: FormDefinition, + form: Blueprint, fileDetails: { name: string; data: Uint8Array; } ) => { const fields = await getDocumentFieldData(fileDetails.data); - const cachedPdf = await getSuggestedFormElementsFromCache(fileDetails.data); + const cachedPdf = await getSuggestedPatternsFromCache(fileDetails.data); if (cachedPdf) { form = updateFormSummary(form, { title: cachedPdf.title, description: '', }); - form = addFormElementMap(form, cachedPdf.elements, cachedPdf.root); + form = addPatternMap(form, cachedPdf.patterns, cachedPdf.root); const updatedForm = addFormOutput(form, { data: fileDetails.data, path: fileDetails.name, @@ -66,86 +66,81 @@ export const addDocument = async ( }; export const addDocumentFieldsToForm = ( - form: FormDefinition, + form: Blueprint, fields: DocumentFieldMap ) => { - const elements: FormElement[] = []; - Object.entries(fields).map(([elementId, field]) => { + const patterns: Pattern[] = []; + Object.entries(fields).map(([patternId, field]) => { if (field.type === 'CheckBox') { - elements.push({ + patterns.push({ type: 'input', - id: elementId, + id: patternId, data: { label: field.label, }, - default: { + initial: { label: '', initial: '', required: false, maxLength: 128, }, - required: field.required, - } satisfies InputElement); + } satisfies InputPattern); } else if (field.type === 'OptionList') { - elements.push({ + patterns.push({ type: 'input', - id: elementId, + id: patternId, data: { label: field.label, }, - default: { + initial: { label: '', initial: '', required: false, maxLength: 128, }, - required: field.required, - } satisfies InputElement); + } satisfies InputPattern); } else if (field.type === 'Dropdown') { - elements.push({ + patterns.push({ type: 'input', - id: elementId, + id: patternId, data: { label: field.label, }, - default: { + initial: { label: '', initial: '', required: false, maxLength: 128, }, - required: field.required, - } satisfies InputElement); + } satisfies InputPattern); } else if (field.type === 'TextField') { - elements.push({ + patterns.push({ type: 'input', - id: elementId, + id: patternId, data: { label: field.label, }, - default: { + initial: { label: '', initial: '', required: false, maxLength: 128, }, - required: field.required, - } satisfies InputElement); + } satisfies InputPattern); } else if (field.type === 'RadioGroup') { - elements.push({ + patterns.push({ type: 'input', - id: elementId, + id: patternId, data: { label: field.label, }, - default: { + initial: { label: '', initial: '', required: false, maxLength: 128, }, - required: field.required, - } satisfies InputElement); + } satisfies InputPattern); } else if (field.type === 'Paragraph') { // skip purely presentational fields } else if (field.type === 'not-supported') { @@ -154,14 +149,13 @@ export const addDocumentFieldsToForm = ( const _exhaustiveCheck: never = field; } }); - elements.push({ + patterns.push({ id: 'root', type: 'sequence', data: { - elements: elements.map(element => element.id), + patterns: patterns.map(pattern => pattern.id), }, - default: [], - required: true, + initial: [], }); - return addFormElements(form, elements, 'root'); + return addPatterns(form, patterns, 'root'); }; diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index 20ef721c7..90e135139 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -9,14 +9,14 @@ export const createFormOutputFieldData = ( formData: Record ): Record => { const results = {} as Record; - Object.entries(output.fields).forEach(([elementId, docField]) => { + Object.entries(output.fields).forEach(([patternId, docField]) => { if (docField.type === 'not-supported') { return; } - const outputFieldId = output.formFields[elementId]; + const outputFieldId = output.formFields[patternId]; results[outputFieldId] = { type: docField.type, - value: formData[elementId], + value: formData[patternId], }; }); return results; diff --git a/packages/forms/src/documents/pdf/mock-api.ts b/packages/forms/src/documents/pdf/mock-api.ts index dddbd6c6f..118d9f5fb 100644 --- a/packages/forms/src/documents/pdf/mock-api.ts +++ b/packages/forms/src/documents/pdf/mock-api.ts @@ -1,17 +1,13 @@ import * as z from 'zod'; -import { - type FormElement, - type FormElementId, - type FormElementMap, -} from '../..'; +import { type Pattern, type PatternId, type PatternMap } from '../..'; -import { ParagraphElement } from '../../config/elements/paragraph'; -import { InputElement } from '../../config/elements/input'; -import { FieldsetElement } from '../../config/elements/fieldset'; +import { type FieldsetPattern } from '../../patterns/fieldset'; +import { type InputPattern } from '../../patterns/input'; +import { type ParagraphPattern } from '../../patterns/paragraph'; import { stringToBase64 } from '../util'; -import { DocumentFieldMap } from '../types'; +import { type DocumentFieldMap } from '../types'; import json from './al_name_change'; @@ -94,28 +90,28 @@ const ExtractedObject = z.object({ type ExtractedObject = z.infer; export type ParsedPdf = { - elements: FormElementMap; + patterns: PatternMap; outputs: DocumentFieldMap; // to populate FormOutput - root: FormElementId; + root: PatternId; title: string; }; export const parseAlabamaNameChangeForm = (): ParsedPdf => { const extracted: ExtractedObject = ExtractedObject.parse(json); const parsedPdf: ParsedPdf = { - elements: {}, + patterns: {}, outputs: {}, root: 'root', title: extracted.title, }; - const rootSequence: FormElementId[] = []; + const rootSequence: PatternId[] = []; for (const element of extracted.elements) { - const fieldsetElements: FormElementId[] = []; + const fieldsetPatterns: PatternId[] = []; if (element.inputs.length === 0) { - parsedPdf.elements[element.id] = { + parsedPdf.patterns[element.id] = { type: 'paragraph', id: element.id, - default: { + initial: { text: '', maxLength: 2048, }, @@ -123,18 +119,17 @@ export const parseAlabamaNameChangeForm = (): ParsedPdf => { text: element.element_params.text, style: element.element_params.text_style, }, - required: false, - } satisfies ParagraphElement; + } satisfies ParagraphPattern; rootSequence.push(element.id); continue; } for (const input of element.inputs) { if (input.input_type === 'Tx') { const id = stringToBase64(PdfFieldMap[input.input_params.output_id]); - parsedPdf.elements[id] = { + parsedPdf.patterns[id] = { type: 'input', id, - default: { + initial: { required: false, label: '', initial: '', @@ -143,9 +138,8 @@ export const parseAlabamaNameChangeForm = (): ParsedPdf => { data: { label: input.input_params.instructions, }, - required: false, - } satisfies InputElement; - fieldsetElements.push(id); + } satisfies InputPattern; + fieldsetPatterns.push(id); parsedPdf.outputs[id] = { type: 'TextField', name: PdfFieldMap[input.input_params.output_id], @@ -156,53 +150,50 @@ export const parseAlabamaNameChangeForm = (): ParsedPdf => { }; } } - if (fieldsetElements.length > 0) { - parsedPdf.elements[element.id] = { + if (fieldsetPatterns.length > 0) { + parsedPdf.patterns[element.id] = { id: element.id, type: 'fieldset', data: { legend: element.element_params.text, - elements: fieldsetElements, + patterns: fieldsetPatterns, }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, - } as FieldsetElement; + } as FieldsetPattern; rootSequence.push(element.id); } } - parsedPdf.elements['root'] = { + parsedPdf.patterns['root'] = { id: 'root', type: 'sequence', data: { - elements: rootSequence, + patterns: rootSequence, }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, }; return parsedPdf; }; -const getElementInputs = (element: ExtractedElement): FormElement[] => { +const getElementInputs = (element: ExtractedElement): Pattern[] => { return element.inputs .map((input: ExtractedInput) => { if (input.input_type === 'Tx') { return { type: 'input', id: input.input_params.output_id, - default: {} as unknown as any, + initial: {} as unknown as any, data: { label: input.input_params.instructions, }, - required: true, - } satisfies InputElement; + } satisfies InputPattern; } - return null as unknown as FormElement; + return null as unknown as Pattern; }) - .filter((item): item is NonNullable => item !== null); + .filter((item): item is NonNullable => item !== null); }; const PdfFieldMap: Record = { @@ -277,7 +268,7 @@ function parseInputs( function parseElements( pdfElements: ExtractedJsonType['elements'] -): FormElement[] { +): Pattern[] { const output = pdfElements.reduce((acc, element) => { const elementOutput = { type: 'Paragraph', diff --git a/packages/forms/src/documents/suggestions.ts b/packages/forms/src/documents/suggestions.ts index 4445199c0..3103f297f 100644 --- a/packages/forms/src/documents/suggestions.ts +++ b/packages/forms/src/documents/suggestions.ts @@ -9,7 +9,7 @@ export type SuggestedForm = { type?: 'text'; }[]; -export const getSuggestedFormElementsFromCache = async ( +export const getSuggestedPatternsFromCache = async ( rawData: Uint8Array ): Promise => { const cache = getFakeCache(); diff --git a/packages/forms/src/element.ts b/packages/forms/src/element.ts deleted file mode 100644 index 8a5d16bdf..000000000 --- a/packages/forms/src/element.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { type Result } from '@atj/common'; -import { - type FormElementConfig, - type FormConfig, - type FormDefinition, -} from '..'; - -export type FormElement = { - type: string; - id: FormElementId; - data: C; - default: T; - required: boolean; -}; - -export type FormElementId = string; -export type FormElementValue = - T['default']; -export type FormElementValueMap = Record; -export type FormElementMap = Record; -export type GetFormElement = ( - form: FormDefinition, - id: FormElementId -) => FormElement; - -export type ParseFormElementData = ( - elementData: T['data'], - obj: string -) => Result; - -export type ParseFormElementConfigData = ( - elementData: T['data'] -) => Result; - -export const getFormElement: GetFormElement = (form, elementId) => { - return form.elements[elementId]; -}; - -export const getFormElementMap = (elements: FormElement[]) => { - return Object.fromEntries( - elements.map(element => { - return [element.id, element]; - }) - ); -}; - -export const getFormElements = ( - form: FormDefinition, - elementIds: FormElementId[] -) => { - return elementIds.map(elementId => getFormElement(form, elementId)); -}; - -export const getFormElementConfig = ( - config: FormConfig, - elementType: FormElement['type'] -) => { - return config.elements[elementType]; -}; - -export const validateElement = ( - elementConfig: FormElementConfig, - element: FormElement, - value: any -): Result => { - if (!elementConfig.acceptsInput) { - return { - success: true, - data: value, - }; - } - const parseResult = elementConfig.parseData(element, value); - if (!parseResult.success) { - return { - success: false, - error: parseResult.error, - }; - } - if (element.data.required && !parseResult.data) { - return { - success: false, - error: 'Required value not provided.', - }; - } - return { - success: true, - data: parseResult.data, - }; -}; - -export const getFirstFormElement = ( - config: FormConfig, - form: FormDefinition, - element?: FormElement -): FormElement => { - if (!element) { - element = form.elements[form.root]; - } - const elemConfig = getFormElementConfig(config, element.type); - const children = elemConfig.getChildren(element, form.elements); - if (children?.length === 0) { - return element; - } - return getFirstFormElement(config, form, children[0]); -}; diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 6df46d1fe..4a803db36 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -1,48 +1,45 @@ -import { FormConfig } from './config'; -import { SequenceElement } from './config/elements/sequence'; -import { DocumentFieldMap } from './documents'; +import { type SequencePattern } from './patterns/sequence'; +import { type DocumentFieldMap } from './documents'; import { - type FormElement, - type FormElementId, - type FormElementMap, - type FormElementValue, - type FormElementValueMap, - getFormElementMap, - getFormElementConfig, -} from './element'; + type FormConfig, + type Pattern, + type PatternId, + type PatternMap, + getPatternMap, + getPatternConfig, +} from './pattern'; export * from './builder'; +export * from './components'; export * from './config'; export * from './documents'; -export * from './element'; export * from './pattern'; export * from './response'; export * from './session'; -export type FormDefinition = { +export type Blueprint = { summary: FormSummary; - root: FormElementId; - elements: FormElementMap; + root: PatternId; + patterns: PatternMap; outputs: FormOutput[]; }; -export const nullFormDefinition: FormDefinition = { +export const nullBlueprint: Blueprint = { summary: { title: '', description: '', }, root: 'root', - elements: { + patterns: { root: { type: 'sequence', id: 'root', data: { - elements: [], + patterns: [], }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, }, }, outputs: [], @@ -53,17 +50,6 @@ export type FormSummary = { description: string; }; -export type FormSessionId = string; -type ErrorMap = Record; -export type FormSession = { - id: FormSessionId; - data: { - errors: ErrorMap; - values: FormElementValueMap; - }; - form: FormDefinition; -}; - export type FormOutput = { data: Uint8Array; path: string; @@ -74,75 +60,56 @@ export type FormOutput = { export const createForm = ( summary: FormSummary, initial: { - elements: FormElement[]; - root: FormElementId; + patterns: Pattern[]; + root: PatternId; } = { - elements: [ + patterns: [ { id: 'root', type: 'sequence', data: { - elements: [], + patterns: [], }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, - } satisfies SequenceElement, + } satisfies SequencePattern, ], root: 'root', } -): FormDefinition => { +): Blueprint => { return { summary, root: initial.root, - elements: getFormElementMap(initial.elements), + patterns: getPatternMap(initial.patterns), outputs: [], }; }; -export const getRootFormElement = (form: FormDefinition) => { - return form.elements[form.root]; -}; - -export const createFormSession = (form: FormDefinition): FormSession => { - return { - id: crypto.randomUUID(), - data: { - errors: {}, - values: Object.fromEntries( - Object.values(form.elements).map((element, index) => { - return [element.id, form.elements[element.id].data.initial]; - }) - ), - }, - form, - }; +export const getRootPattern = (form: Blueprint) => { + return form.patterns[form.root]; }; -export const updateForm = ( - context: FormSession, - id: FormElementId, - value: any -) => { - if (!(id in context.form.elements)) { - console.error(`FormElement "${id}" does not exist on form.`); +/* +export const updateForm = (context: Session, id: PatternId, value: any) => { + if (!(id in context.form.patterns)) { + console.error(`Pattern "${id}" does not exist on form.`); return context; } const nextForm = addValue(context, id, value); - const element = context.form.elements[id]; - if (element.type === 'input') { - if (element.data.required && !value) { + const pattern = context.form.patterns[id]; + if (pattern.type === 'input') { + if (pattern.data.required && !value) { return addError(nextForm, id, 'Required value not provided.'); } } return nextForm; }; -const addValue = ( +const addValue = ( form: FormSession, - id: FormElementId, - value: FormElementValue + id: PatternId, + value: PatternValue ): FormSession => ({ ...form, data: { @@ -156,7 +123,7 @@ const addValue = ( const addError = ( session: FormSession, - id: FormElementId, + id: PatternId, error: string ): FormSession => ({ ...session, @@ -168,112 +135,88 @@ const addError = ( }, }, }); +*/ -export const addFormElementMap = ( - form: FormDefinition, - elements: FormElementMap, - root?: FormElementId +export const addPatternMap = ( + form: Blueprint, + patterns: PatternMap, + root?: PatternId ) => { return { ...form, - elements: { ...form.elements, ...elements }, + patterns: { ...form.patterns, ...patterns }, root: root !== undefined ? root : form.root, }; }; -export const addFormElements = ( - form: FormDefinition, - elements: FormElement[], - root?: FormElementId +export const addPatterns = ( + form: Blueprint, + patterns: Pattern[], + root?: PatternId ) => { - const formElementMap = getFormElementMap(elements); - return addFormElementMap(form, formElementMap, root); + const formPatternMap = getPatternMap(patterns); + return addPatternMap(form, formPatternMap, root); }; -export const replaceFormElements = ( - form: FormDefinition, - elements: FormElement[] -): FormDefinition => { +export const replacePatterns = ( + form: Blueprint, + patterns: Pattern[] +): Blueprint => { return { ...form, - elements: elements.reduce( - (acc, element) => { - acc[element.id] = element; + patterns: patterns.reduce( + (acc, pattern) => { + acc[pattern.id] = pattern; return acc; }, - {} as Record + {} as Record ), }; }; -export const updateElements = ( +export const updatePatterns = ( config: FormConfig, - form: FormDefinition, - newElements: FormElementMap -): FormDefinition => { - const root = newElements[form.root]; - const targetElements: FormElementMap = { + form: Blueprint, + newPatterns: PatternMap +): Blueprint => { + const root = newPatterns[form.root]; + const targetPatterns: PatternMap = { root, }; - const resource = config.elements[root.type as keyof FormConfig]; - const children = resource.getChildren(root, newElements); - targetElements[root.id] = root; - children.forEach(child => (targetElements[child.id] = child)); + const resource = config.patterns[root.type as keyof FormConfig]; + const children = resource.getChildren(root, newPatterns); + targetPatterns[root.id] = root; + children.forEach(child => (targetPatterns[child.id] = child)); return { ...form, - elements: targetElements, + patterns: targetPatterns, }; }; -export const updateElement = ( - form: FormDefinition, - formElement: FormElement -): FormDefinition => { +export const updatePattern = (form: Blueprint, pattern: Pattern): Blueprint => { return { ...form, - elements: { - ...form.elements, - [formElement.id]: formElement, + patterns: { + ...form.patterns, + [pattern.id]: pattern, }, }; }; -export const addFormOutput = (form: FormDefinition, document: FormOutput) => { +export const addFormOutput = (form: Blueprint, document: FormOutput) => { return { ...form, outputs: [...form.outputs, document], }; }; -export const getFormElement = (form: FormDefinition, id: FormElementId) => { - return form.elements[id]; +export const getPattern = (form: Blueprint, id: PatternId) => { + return form.patterns[id]; }; -export const updateFormSummary = ( - form: FormDefinition, - summary: FormSummary -) => { +export const updateFormSummary = (form: Blueprint, summary: FormSummary) => { return { ...form, summary, }; }; - -export const updateFormElement = ( - config: FormConfig, - form: FormDefinition, - formElement: FormElement, - formData: FormElementMap -) => { - const elementConfig = getFormElementConfig(config, formElement.type); - const data = formData[formElement.id].data; - const result = elementConfig.parseConfigData(data); - if (!result.success) { - return; - } - const updatedForm = updateElement(form, { - ...formElement, - data: result.data, - }); - return updatedForm; -}; diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index 7657ee57e..6a6b7d8b4 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -1,164 +1,133 @@ -import { - type FormConfig, - type FormElement, - type FormElementId, - getRootFormElement, -} from '..'; -import { getFormElementConfig } from './element'; -import { type FormSession, nullSession, sessionIsComplete } from './session'; +import { type Result } from '@atj/common'; +import { updatePattern, type Blueprint } from '..'; -export type TextInputPattern = { - type: 'input'; - inputId: string; - value: string; - label: string; - required: boolean; - error?: string; -}; +import { type CreatePrompt } from './components'; -export type TextPrompt = { - type: 'text'; - id: string; - value: string; - error?: string; +export type Pattern = { + type: string; + id: PatternId; + data: C; + initial: T; }; -export type FormSummaryPattern = { - type: 'form-summary'; - title: string; - description: string; -}; +export type PatternId = string; +export type PatternValue = T['initial']; +export type PatternValueMap = Record; +export type PatternMap = Record; +export type GetPattern = (form: Blueprint, id: PatternId) => Pattern; -export type SubmissionConfirmationPattern = { - type: 'submission-confirmation'; - table: { label: string; value: string }[]; -}; +export type ParsePatternData = ( + patternData: T['data'], + obj: string +) => Result; -export type ParagraphPattern = { - type: 'paragraph'; - text: string; - style: 'indent' | 'normal' | 'heading' | 'subheading'; +export type ParsePatternConfigData = ( + patternData: T['data'] +) => Result; + +export const getPattern: GetPattern = (form, patternId) => { + return form.patterns[patternId]; }; -export type FieldsetPattern = { - type: 'fieldset'; - legend: string; +export type PatternConfig = { + acceptsInput: boolean; + initial: ThisPattern['data']; + parseData: ParsePatternData; + parseConfigData: ParsePatternConfigData; + getChildren: ( + pattern: ThisPattern, + patterns: Record + ) => Pattern[]; + createPrompt: CreatePrompt; +}; +export type FormConfig = { + patterns: Record>; }; -export type Pattern = { - _elementId: FormElementId; - _children: PromptPart[]; - type: string; -} & T; +export type ConfigPatterns = ReturnType< + Config['patterns'][keyof Config['patterns']]['parseData'] +>; -export type SubmitAction = { - type: 'submit'; - text: 'Submit'; +export const getPatternMap = (patterns: Pattern[]) => { + return Object.fromEntries( + patterns.map(pattern => { + return [pattern.id, pattern]; + }) + ); }; -export type PromptAction = SubmitAction; -export type PromptPart = { - pattern: Pattern; - children: PromptPart[]; +export const getPatterns = (form: Blueprint, patternIds: PatternId[]) => { + return patternIds.map(patternIds => getPattern(form, patternIds)); }; -export type Prompt = { - actions: PromptAction[]; - parts: PromptPart[]; +export const getPatternConfig = ( + config: FormConfig, + elementType: Pattern['type'] +) => { + return config.patterns[elementType]; }; -export const createPrompt = ( - config: FormConfig, - session: FormSession, - options: { validate: boolean } -): Prompt => { - if (options.validate && sessionIsComplete(config, session)) { +export const validatePattern = ( + elementConfig: PatternConfig, + element: Pattern, + value: any +): Result => { + if (!elementConfig.acceptsInput) { return { - actions: [], - parts: [ - { - pattern: { - _elementId: 'submission-confirmation', - type: 'submission-confirmation', - table: Object.entries(session.data.values) - .filter(([elementId, value]) => { - const elemConfig = getFormElementConfig( - config, - session.form.elements[elementId].type - ); - return elemConfig.acceptsInput; - }) - .map(([elementId, value]) => { - return { - label: session.form.elements[elementId].data.label, - value: value, - }; - }), - } as Pattern, - children: [], - }, - ], + success: true, + data: value, + }; + } + const parseResult = elementConfig.parseData(element, value); + if (!parseResult.success) { + return { + success: false, + error: parseResult.error, + }; + } + if (element.data.required && !parseResult.data) { + return { + success: false, + error: 'Required value not provided.', }; } - const parts: PromptPart[] = [ - { - pattern: { - _elementId: 'form-summary', - type: 'form-summary', - title: session.form.summary.title, - description: session.form.summary.description, - } as Pattern, - children: [], - }, - ]; - const root = getRootFormElement(session.form); - parts.push(createPromptForElement(config, session, root, options)); return { - actions: [ - { - type: 'submit', - text: 'Submit', - }, - ], - parts, + success: true, + data: parseResult.data, }; }; -export type CreatePrompt = ( +export const getFirstPattern = ( config: FormConfig, - session: FormSession, - element: T, - options: { validate: boolean } -) => PromptPart; - -export const createPromptForElement: CreatePrompt = ( - config, - session, - element, - options -) => { - const formElementConfig = getFormElementConfig(config, element.type); - return formElementConfig.createPrompt(config, session, element, options); -}; - -export const isPromptAction = (prompt: Prompt, action: string) => { - return prompt.actions.find(a => a.type === action); + form: Blueprint, + pattern?: Pattern +): Pattern => { + if (!pattern) { + pattern = form.patterns[form.root]; + } + const elemConfig = getPatternConfig(config, pattern.type); + const children = elemConfig.getChildren(pattern, form.patterns); + if (children?.length === 0) { + return pattern; + } + return getFirstPattern(config, form, children[0]); }; -export const createNullPrompt = ({ - config, - element, -}: { - config: FormConfig; - element: FormElement; -}): Prompt => { - const formElementConfig = getFormElementConfig(config, element.type); - return { - parts: [ - formElementConfig.createPrompt(config, nullSession, element, { - validate: false, - }), - ], - actions: [], - }; +export const updatePatternFromFormData = ( + config: FormConfig, + form: Blueprint, + pattern: Pattern, + formData: PatternMap +) => { + const elementConfig = getPatternConfig(config, pattern.type); + const data = formData[pattern.id].data; + const result = elementConfig.parseConfigData(data); + if (!result.success) { + return; + } + const updatedForm = updatePattern(form, { + ...pattern, + data: result.data, + }); + return updatedForm; }; diff --git a/packages/forms/src/patterns/fieldset.ts b/packages/forms/src/patterns/fieldset.ts new file mode 100644 index 000000000..4a713caf5 --- /dev/null +++ b/packages/forms/src/patterns/fieldset.ts @@ -0,0 +1,53 @@ +import * as z from 'zod'; + +import { + type Pattern, + type PatternConfig, + type PatternId, + getPattern, +} from '../pattern'; +import { type FieldsetProps, createPromptForPattern } from '../components'; +import { safeZodParse } from '../util/zod'; + +export type FieldsetPattern = Pattern<{ + legend?: string; + patterns: PatternId[]; +}>; + +const FieldsetSchema = z.array(z.string()); + +const configSchema = z.object({ + legend: z.string().optional(), + patterns: z.array(z.string()), +}); + +export const fieldsetConfig: PatternConfig = { + acceptsInput: false, + initial: { + patterns: [], + }, + parseData: (_, obj) => { + return safeZodParse(FieldsetSchema, obj); + }, + parseConfigData: obj => safeZodParse(configSchema, obj), + getChildren(pattern, patterns) { + return pattern.data.patterns.map( + (patternId: string) => patterns[patternId] + ); + }, + createPrompt(config, session, pattern, options) { + const children = pattern.data.patterns.map((patternId: string) => { + const pattern = getPattern(session.form, patternId); + return createPromptForPattern(config, session, pattern, options); + }); + return { + pattern: { + _children: children, + _patternId: pattern.id, + type: 'fieldset', + legend: pattern.data.legend, + } satisfies FieldsetProps, + children, + }; + }, +}; diff --git a/packages/forms/src/patterns/form-summary.ts b/packages/forms/src/patterns/form-summary.ts new file mode 100644 index 000000000..d58cd9e9f --- /dev/null +++ b/packages/forms/src/patterns/form-summary.ts @@ -0,0 +1,37 @@ +import * as z from 'zod'; + +import { type Pattern, type PatternConfig } from '../pattern'; +import { type FormSummaryProps } from '../components'; +import { safeZodParse } from '../util/zod'; + +const configSchema = z.object({ + title: z.string().max(128), + summary: z.string().max(2024), +}); +export type FormSummary = Pattern>; + +export const formSummaryConfig: PatternConfig = { + acceptsInput: false, + initial: { + text: '', + initial: '', + required: true, + maxLength: 128, + }, + parseData: obj => safeZodParse(configSchema, obj), // make this optional? + parseConfigData: obj => safeZodParse(configSchema, obj), + getChildren() { + return []; + }, + createPrompt(_, session, pattern, options) { + return { + pattern: { + _patternId: pattern.id, + type: 'form-summary', + title: pattern.data.title, + description: pattern.data.description, + } as FormSummaryProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/config/config.ts b/packages/forms/src/patterns/index.ts similarity index 58% rename from packages/forms/src/config/config.ts rename to packages/forms/src/patterns/index.ts index f3d19ed80..706e036b0 100644 --- a/packages/forms/src/config/config.ts +++ b/packages/forms/src/patterns/index.ts @@ -1,14 +1,15 @@ -import { type FormConfig } from '.'; -import { fieldsetConfig } from './elements/fieldset'; -import { inputConfig } from './elements/input'; -import { paragraphConfig } from './elements/paragraph'; -import { sequenceConfig } from './elements/sequence'; +import { type FormConfig } from '../pattern'; + +import { fieldsetConfig } from './fieldset'; +import { inputConfig } from './input'; +import { paragraphConfig } from './paragraph'; +import { sequenceConfig } from './sequence'; // This configuration reflects what a user of this library would provide for // their usage scenarios. For now, keep here in the form service until we // understand the usage scenarios better. export const defaultFormConfig: FormConfig = { - elements: { + patterns: { fieldset: fieldsetConfig, input: inputConfig, paragraph: paragraphConfig, diff --git a/packages/forms/src/patterns/input.ts b/packages/forms/src/patterns/input.ts new file mode 100644 index 000000000..c1f6f68c2 --- /dev/null +++ b/packages/forms/src/patterns/input.ts @@ -0,0 +1,54 @@ +import * as z from 'zod'; + +import { type Pattern, type PatternConfig, validatePattern } from '../pattern'; +import { type TextInputProps } from '../components'; +import { getFormSessionValue } from '../session'; +import { safeZodParse } from '../util/zod'; + +const configSchema = z.object({ + label: z.string(), + initial: z.string(), + required: z.boolean(), + maxLength: z.coerce.number(), +}); +export type InputPattern = Pattern>; + +const createSchema = (data: InputPattern['data']) => + z.string().max(data.maxLength); + +export const inputConfig: PatternConfig = { + acceptsInput: true, + initial: { + label: '', + initial: '', + required: true, + maxLength: 128, + }, + parseData: (patternData, obj) => safeZodParse(createSchema(patternData), obj), + parseConfigData: obj => safeZodParse(configSchema, obj), + getChildren() { + return []; + }, + createPrompt(_, session, pattern, options) { + const extraAttributes: Record = {}; + const sessionValue = getFormSessionValue(session, pattern.id); + if (options.validate) { + const isValidResult = validatePattern(inputConfig, pattern, sessionValue); + if (!isValidResult.success) { + extraAttributes['error'] = isValidResult.error; + } + } + return { + pattern: { + _patternId: pattern.id, + type: 'input', + inputId: pattern.id, + value: sessionValue, + label: pattern.data.label, + required: pattern.data.required, + ...extraAttributes, + } as TextInputProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/patterns/paragraph.ts b/packages/forms/src/patterns/paragraph.ts new file mode 100644 index 000000000..e899d67eb --- /dev/null +++ b/packages/forms/src/patterns/paragraph.ts @@ -0,0 +1,38 @@ +import * as z from 'zod'; + +import { type Pattern, type PatternConfig } from '../pattern'; +import { type ParagraphProps } from '../components'; +import { safeZodParse } from '../util/zod'; + +const configSchema = z.object({ + text: z.string(), + maxLength: z.coerce.number(), +}); +export type ParagraphPattern = Pattern>; + +const createSchema = (data: ParagraphPattern['data']) => + z.string().max(data.maxLength); + +export const paragraphConfig: PatternConfig = { + acceptsInput: false, + initial: { + text: 'normal', + maxLength: 2048, + }, + parseData: (patternData, obj) => safeZodParse(createSchema(patternData), obj), + parseConfigData: obj => safeZodParse(configSchema, obj), + getChildren() { + return []; + }, + createPrompt(_, session, pattern, options) { + return { + pattern: { + _patternId: pattern.id, + type: 'paragraph' as const, + text: pattern.data.text, + style: pattern.data.style, + } as ParagraphProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/patterns/sequence.ts b/packages/forms/src/patterns/sequence.ts new file mode 100644 index 000000000..4c8dabeab --- /dev/null +++ b/packages/forms/src/patterns/sequence.ts @@ -0,0 +1,50 @@ +import * as z from 'zod'; + +import { + type Pattern, + type PatternConfig, + type PatternId, + getPattern, +} from '../pattern'; +import { createPromptForPattern } from '../components'; +import { safeZodParse } from '../util/zod'; + +export type SequencePattern = Pattern<{ + patterns: PatternId[]; +}>; + +const sequenceSchema = z.array(z.string()); + +const configSchema = z.object({ + patterns: z.array(z.string()), +}); + +export const sequenceConfig: PatternConfig = { + acceptsInput: false, + initial: { + patterns: [], + }, + parseData: (_, obj) => { + return safeZodParse(sequenceSchema, obj); + }, + parseConfigData: obj => safeZodParse(configSchema, obj), + getChildren(pattern, patterns) { + return pattern.data.patterns.map( + (patternId: string) => patterns[patternId] + ); + }, + 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 { + pattern: { + _children: children, + _patternId: pattern.id, + type: 'sequence', + }, + children, + }; + }, +}; diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index 88274b4ec..3e3db1374 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -2,12 +2,12 @@ import { type Result } from '@atj/common'; import { type FormConfig, - type FormElementId, - getFormElement, - getFormElementConfig, - validateElement, + type PatternId, + getPattern, + getPatternConfig, + validatePattern, } from '.'; -import { type PromptAction, createPrompt, isPromptAction } from './pattern'; +import { type PromptAction, createPrompt, isPromptAction } from './components'; import { type FormSession, updateSession } from './session'; export type PromptResponse = { @@ -37,15 +37,15 @@ export const applyPromptResponse = ( }; }; -const parseElementValue = ( +const parsePatternValue = ( config: FormConfig, session: FormSession, - elementId: FormElementId, + patternId: PatternId, promptValue: string ) => { - const element = session.form.elements[elementId]; - const formElementConfig = getFormElementConfig(config, element.type); - return formElementConfig.parseData(element, promptValue); + const pattern = session.form.patterns[patternId]; + const patternConfig = getPatternConfig(config, pattern.type); + return patternConfig.parseData(pattern, promptValue); }; const parsePromptResponse = ( @@ -55,14 +55,14 @@ const parsePromptResponse = ( ) => { const values: Record = {}; const errors: Record = {}; - for (const [elementId, promptValue] of Object.entries(response.data)) { - const element = getFormElement(session.form, elementId); - const elementConfig = getFormElementConfig(config, element.type); - const isValidResult = validateElement(elementConfig, element, promptValue); + for (const [patternId, promptValue] of Object.entries(response.data)) { + const pattern = getPattern(session.form, patternId); + const patternConfig = getPatternConfig(config, pattern.type); + const isValidResult = validatePattern(patternConfig, pattern, promptValue); if (isValidResult.success) { - values[elementId] = isValidResult.data; + values[patternId] = isValidResult.data; } else { - errors[elementId] = isValidResult.error; + errors[patternId] = isValidResult.error; } } return { errors, values }; diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index 87659159a..4b8543663 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -1,25 +1,25 @@ import { type FormConfig, - type FormDefinition, - type FormElement, - getFormElementConfig, - validateElement, + type Blueprint, + type Pattern, + getPatternConfig, + validatePattern, } from '.'; -import { SequenceElement } from './config/elements/sequence'; +import { SequencePattern } from './patterns/sequence'; import { - type FormElementId, - type FormElementValue, - type FormElementValueMap, -} from './element'; + type PatternId, + type PatternValue, + type PatternValueMap, +} from './pattern'; -type ErrorMap = Record; +type ErrorMap = Record; export type FormSession = { data: { errors: ErrorMap; - values: FormElementValueMap; + values: PatternValueMap; }; - form: FormDefinition; + form: Blueprint; }; export const nullSession: FormSession = { @@ -30,16 +30,16 @@ export const nullSession: FormSession = { }, }, form: { - elements: { + patterns: { root: { id: 'root', type: 'sequence', required: false, - default: { - elements: [], + initial: { + patterns: [], }, data: {}, - } as SequenceElement, + } as SequencePattern, }, root: 'root', summary: { @@ -50,13 +50,13 @@ export const nullSession: FormSession = { }, }; -export const createFormSession = (form: FormDefinition): FormSession => { +export const createFormSession = (form: Blueprint): FormSession => { return { data: { errors: {}, values: Object.fromEntries( - Object.values(form.elements).map(element => { - return [element.id, form.elements[element.id].default]; + Object.values(form.patterns).map((pattern, index) => { + return [pattern.id, form.patterns[pattern.id].data.initial]; }) ), }, @@ -66,24 +66,24 @@ export const createFormSession = (form: FormDefinition): FormSession => { export const getFormSessionValue = ( session: FormSession, - elementId: FormElementId + patternId: PatternId ) => { - return session.data.values[elementId]; + return session.data.values[patternId]; }; export const updateSessionValue = ( session: FormSession, - id: FormElementId, - value: FormElementValue + id: PatternId, + value: PatternValue ): FormSession => { - if (!(id in session.form.elements)) { - console.error(`FormElement "${id}" does not exist on form.`); + if (!(id in session.form.patterns)) { + console.error(`Pattern "${id}" does not exist on form.`); return session; } const nextSession = addValue(session, id, value); - const element = session.form.elements[id]; - if (element.type === 'input') { - if (element.required && !value) { + const pattern = session.form.patterns[id]; + if (pattern.type === 'input') { + if (pattern && !value) { return addError(nextSession, id, 'Required value not provided.'); } } @@ -92,16 +92,16 @@ export const updateSessionValue = ( export const updateSession = ( session: FormSession, - values: FormElementValueMap, + values: PatternValueMap, errors: ErrorMap ): FormSession => { const keysValid = Object.keys(values).every( - elementId => elementId in session.form.elements + patternId => patternId in session.form.patterns ) && - Object.keys(errors).every(elementId => elementId in session.form.elements); + Object.keys(errors).every(patternId => patternId in session.form.patterns); if (!keysValid) { - throw new Error('invalid element reference updating session'); + throw new Error('invalid pattern reference updating session'); } return { ...session, @@ -119,18 +119,18 @@ export const updateSession = ( }; export const sessionIsComplete = (config: FormConfig, session: FormSession) => { - return Object.values(session.form.elements).every(element => { - const elementConfig = getFormElementConfig(config, element.type); - const value = getFormSessionValue(session, element.id); - const isValidResult = validateElement(elementConfig, element, value); + return Object.values(session.form.patterns).every(pattern => { + const patternConfig = getPatternConfig(config, pattern.type); + const value = getFormSessionValue(session, pattern.id); + const isValidResult = validatePattern(patternConfig, pattern, value); return isValidResult.success; }); }; -const addValue = ( +const addValue = ( form: FormSession, - id: FormElementId, - value: FormElementValue + id: PatternId, + value: PatternValue ): FormSession => ({ ...form, data: { @@ -144,7 +144,7 @@ const addValue = ( const addError = ( session: FormSession, - id: FormElementId, + id: PatternId, error: string ): FormSession => ({ ...session, diff --git a/packages/forms/src/transform/index.ts b/packages/forms/src/util/transform.ts similarity index 100% rename from packages/forms/src/transform/index.ts rename to packages/forms/src/util/transform.ts diff --git a/packages/forms/src/util/zod.ts b/packages/forms/src/util/zod.ts index 8f6ba5fea..262bfe5bb 100644 --- a/packages/forms/src/util/zod.ts +++ b/packages/forms/src/util/zod.ts @@ -2,9 +2,9 @@ import * as z from 'zod'; import { type Result } from '@atj/common'; -import { type FormElement } from '..'; +import { type Pattern } from '..'; -export const safeZodParse = ( +export const safeZodParse = ( schema: z.Schema, obj: string ): Result => { diff --git a/packages/forms/tests/two-field-form.test.ts b/packages/forms/tests/two-field-form.test.ts index 4b0cf98ec..42bc70b18 100644 --- a/packages/forms/tests/two-field-form.test.ts +++ b/packages/forms/tests/two-field-form.test.ts @@ -2,47 +2,44 @@ import { describe, expect, test } from 'vitest'; import * as forms from '../src'; -const elements: forms.FormElement[] = [ +const patterns: forms.Pattern[] = [ { type: 'sequence', id: 'root', data: { - elements: ['element-1', 'element-2'], + patterns: ['pattern-1', 'pattern-2'], }, - default: { - elements: [], + initial: { + patterns: [], }, - required: true, }, { type: 'input', - id: 'element-1', + id: 'pattern-1', data: { text: 'What is your first name?', initial: '', required: true, }, - default: '', - required: true, + initial: '', }, { type: 'input', - id: 'element-2', + id: 'pattern-2', data: { text: 'What is your favorite word?', initial: '', required: false, }, - default: '', - required: true, + initial: '', }, ]; const form = forms.createForm( { title: 'Form sample', - description: 'Form sample created via a list of elements.', + description: 'Form sample created via a list of patterns.', }, - { root: 'root', elements } + { root: 'root', patterns } ); describe('two element form session', () => { @@ -60,16 +57,16 @@ describe('two element form session', () => { test('empty field value on required field is stored with error', () => { const session = forms.createFormSession(form); - const nextSession = forms.updateForm(session, elements[0].id, null); + const nextSession = forms.updateForm(session, patterns[0].id, null); expect(nextSession).toEqual({ ...session, data: { errors: { - 'element-1': 'Required value not provided.', + 'pattern-1': 'Required value not provided.', }, values: { - 'element-1': null, - 'element-2': '', + 'pattern-1': null, + 'pattern-2': '', }, }, }); @@ -79,7 +76,7 @@ describe('two element form session', () => { const formSession = forms.createFormSession(form); const nextSession = forms.updateForm( formSession, - elements[0].id, + patterns[0].id, 'supercalifragilisticexpialidocious' ); expect(nextSession).toEqual({ @@ -87,8 +84,8 @@ describe('two element form session', () => { data: { errors: {}, values: { - 'element-1': 'supercalifragilisticexpialidocious', - 'element-2': '', + 'pattern-1': 'supercalifragilisticexpialidocious', + 'pattern-2': '', }, }, }); @@ -98,17 +95,17 @@ describe('two element form session', () => { const session = forms.createFormSession(form); const session2 = forms.updateForm( session, - elements[1].id, + patterns[1].id, 'supercalifragilisticexpialidocious' ); - const session3 = forms.updateForm(session2, elements[1].id, ''); + const session3 = forms.updateForm(session2, patterns[1].id, ''); expect(session3).toEqual({ ...session, data: { errors: {}, values: { - 'element-1': '', - 'element-2': '', + 'pattern-1': '', + 'pattern-2': '', }, }, }); @@ -118,7 +115,7 @@ describe('two element form session', () => { const session = forms.createFormSession(form); const nextSession = forms.updateForm( session, - elements[1].id, + patterns[1].id, 'supercalifragilisticexpialidocious' ); expect(nextSession).toEqual({ @@ -126,8 +123,8 @@ describe('two element form session', () => { data: { errors: {}, values: { - 'element-1': '', - 'element-2': 'supercalifragilisticexpialidocious', + 'pattern-1': '', + 'pattern-2': 'supercalifragilisticexpialidocious', }, }, });