diff --git a/.gitignore b/.gitignore index b168fc06..7f174c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage/ html/ node_modules/ NOTES.md +tsconfig.tsbuildinfo diff --git a/apps/spotlight/src/components/AppFormManager.tsx b/apps/spotlight/src/components/AppFormManager.tsx index 020464bc..ad669db0 100644 --- a/apps/spotlight/src/components/AppFormManager.tsx +++ b/apps/spotlight/src/components/AppFormManager.tsx @@ -1,9 +1,19 @@ import React from 'react'; -import { FormManager } from '@atj/design'; +import { FormManager, defaultFormElementComponent } from '@atj/design'; + import { getAppContext } from '../context'; export default function () { const ctx = getAppContext(); - return <FormManager formService={ctx.formService} baseUrl={ctx.baseUrl} />; + return ( + <FormManager + context={{ + config: ctx.formConfig, + components: defaultFormElementComponent, + }} + formService={ctx.formService} + baseUrl={ctx.baseUrl} + /> + ); } diff --git a/apps/spotlight/src/components/AppFormRouter.tsx b/apps/spotlight/src/components/AppFormRouter.tsx index 09cbc11d..9cff368f 100644 --- a/apps/spotlight/src/components/AppFormRouter.tsx +++ b/apps/spotlight/src/components/AppFormRouter.tsx @@ -5,5 +5,5 @@ import { getAppContext } from '../context'; export default function AppFormRouter() { const ctx = getAppContext(); - return <FormRouter formService={ctx.formService} />; + return <FormRouter config={ctx.formConfig} formService={ctx.formService} />; } diff --git a/apps/spotlight/src/components/Footer.astro b/apps/spotlight/src/components/Footer.astro index 02a2d1c7..f11a3421 100644 --- a/apps/spotlight/src/components/Footer.astro +++ b/apps/spotlight/src/components/Footer.astro @@ -18,25 +18,14 @@ const { github } = Astro.props; <nav class="usa-footer__nav" aria-label="Footer navigation,"> <ul class="grid-row grid-gap"> <li - class=" - mobile-lg:grid-col-6 - desktop:grid-col-auto - usa-footer__primary-content - " + class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content" > - <a - class="usa-footer__primary-link" - href="https://10x.gsa.gov/" - > + <a class="usa-footer__primary-link" href="https://10x.gsa.gov/"> 10x </a> </li> <li - class=" - mobile-lg:grid-col-6 - desktop:grid-col-auto - usa-footer__primary-content - " + class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content" > <a class="usa-footer__primary-link" diff --git a/apps/spotlight/src/context.ts b/apps/spotlight/src/context.ts index 32e8c230..12330e79 100644 --- a/apps/spotlight/src/context.ts +++ b/apps/spotlight/src/context.ts @@ -1,13 +1,17 @@ +import { FormConfig } from '@atj/forms'; +import { defaultFormConfig } from '@atj/forms'; import { type FormService, createBrowserFormService, createTestFormService, } from '@atj/form-service'; + import { type GithubRepository } from './lib/github'; export type AppContext = { baseUrl: `${string}/`; github: GithubRepository; + formConfig: FormConfig; formService: FormService; }; @@ -24,6 +28,7 @@ const createAppContext = (env: any) => { return { github: env.GITHUB, baseUrl: env.BASE_URL, + formConfig: defaultFormConfig, formService: createAppFormService(), }; }; diff --git a/package.json b/package.json index aa2f1d94..a3aad8cf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@types/node": "^20.11.16", + "@vitest/coverage-c8": "^0.33.0", "@vitest/coverage-v8": "^1.2.2", "@vitest/ui": "^1.2.2", "eslint": "^8.56.0", diff --git a/packages/design/.eslintrc.cjs b/packages/design/.eslintrc.cjs index 1e1167f2..0d5fa822 100644 --- a/packages/design/.eslintrc.cjs +++ b/packages/design/.eslintrc.cjs @@ -1,35 +1,31 @@ module.exports = { - "env": { - "browser": true, - "es2021": true + env: { + browser: true, + es2021: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + ], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended" - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "react" - ], - "rules": { - } -} + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'react'], + rules: { + 'react/prop-types': 'off', + }, +}; diff --git a/packages/design/src/Form/Form.stories.tsx b/packages/design/src/Form/Form.stories.tsx index e2713a34..a8e834a9 100644 --- a/packages/design/src/Form/Form.stories.tsx +++ b/packages/design/src/Form/Form.stories.tsx @@ -2,13 +2,14 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import Form from '.'; -import { createTestForm } from '../test-form'; +import { createTestForm, createTestFormConfig } from '../test-form'; export default { title: 'Form', component: Form, decorators: [(Story, args) => <Story {...args} />], args: { + config: createTestFormConfig(), form: createTestForm(), }, tags: ['autodocs'], diff --git a/packages/design/src/Form/index.tsx b/packages/design/src/Form/index.tsx index 33c40f5a..77e13fca 100644 --- a/packages/design/src/Form/index.tsx +++ b/packages/design/src/Form/index.tsx @@ -4,6 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { createFormSession, createPrompt, + type FormConfig, type FormDefinition, } from '@atj/forms'; @@ -11,14 +12,16 @@ import PromptSegment from './PromptSegment'; import ActionBar from './ActionBar'; export default function Form({ + config, form, onSubmit, }: { + config: FormConfig; form: FormDefinition; onSubmit?: (data: Record<string, string>) => void; }) { const session = createFormSession(form); - const prompt = createPrompt(session); + const prompt = createPrompt(config, session); const formMethods = useForm<Record<string, string>>({}); return ( diff --git a/packages/design/src/FormManager/DocumentImporter/index.tsx b/packages/design/src/FormManager/DocumentImporter/index.tsx index c06c1536..db71c672 100644 --- a/packages/design/src/FormManager/DocumentImporter/index.tsx +++ b/packages/design/src/FormManager/DocumentImporter/index.tsx @@ -3,7 +3,11 @@ import { useNavigate } from 'react-router-dom'; import { addDocument, addDocumentFieldsToForm } from '@atj/documents'; import { type FormService } from '@atj/form-service'; -import { type DocumentFieldMap, type FormDefinition } from '@atj/forms'; +import { + type FormConfig, + type DocumentFieldMap, + type FormDefinition, +} from '@atj/forms'; import { onFileInputChangeGetFile } from '../FormList/PDFFileSelect/file-input'; import Form from '../../Form'; @@ -11,11 +15,13 @@ import Form from '../../Form'; const DocumentImporter = ({ baseUrl, formId, + config, form, formService, }: { baseUrl: string; formId: string; + config: FormConfig; form: FormDefinition; formService: FormService; }) => { @@ -146,6 +152,7 @@ const DocumentImporter = ({ return ( <> <Form + config={config} form={previewForm} onSubmit={data => { //handleFormSubmission(formId, data); diff --git a/packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx index 852b86e8..7ed8dffd 100644 --- a/packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx +++ b/packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx @@ -5,7 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { createTestFormService } from '@atj/form-service'; import FormEdit from '.'; -import { createTestForm } from '../../test-form'; +import { createTestForm, createTestFormContext } from '../../test-form'; export default { title: 'FormManager/FormEdit', @@ -18,6 +18,7 @@ export default { ), ], args: { + context: createTestFormContext(), formId: 'test-form', formService: createTestFormService({ 'test-form': createTestForm(), diff --git a/packages/design/src/FormManager/FormEdit/FormElementEdit/RenderField.tsx b/packages/design/src/FormManager/FormEdit/FormElementEdit/RenderField.tsx deleted file mode 100644 index 26341ac4..00000000 --- a/packages/design/src/FormManager/FormEdit/FormElementEdit/RenderField.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { useFormContext } from 'react-hook-form'; - -import { FormDefinition, FormElement } from '@atj/forms'; -import { SequenceElementEdit } from './SequenceElementEdit'; - -export default function RenderField({ - form, - element, -}: { - form: FormDefinition; - element: FormElement; -}) { - const { register } = useFormContext(); - const fieldId = element.id; - if (element.type === 'input') { - return ( - <div className="grid-row grid-gap"> - <div className="grid-col"> - <label className="usa-label"> - Input type - <select className="usa-select" {...register(`${fieldId}.type`)}> - <option value={'input'}>Input</option> - <option value={'textarea'}>Textarea</option> - </select> - </label> - </div> - <div className="grid-col"> - <label className="usa-label"> - Field label - <input - className="usa-input" - {...register(`${fieldId}.text`)} - type="text" - ></input> - </label> - </div> - <div className="grid-col"> - <label className="usa-label"> - Default value - <input - className="usa-input" - type="text" - {...register(`${fieldId}.initial`)} - ></input> - </label> - </div> - <div className="grid-col"> - <div className="usa-checkbox"> - <input - className="usa-checkbox__input" - type="checkbox" - id={`${fieldId}.required`} - {...register(`${fieldId}.required`)} - /> - <label - className="usa-checkbox__label" - htmlFor={`${fieldId}.required`} - > - Required - </label> - </div> - </div> - </div> - ); - } else if (element.type == 'sequence') { - return ( - <fieldset> - <SequenceElementEdit element={element} form={form} /> - </fieldset> - ); - } else { - const _exhaustiveCheck: never = element; - return _exhaustiveCheck; - } -} diff --git a/packages/design/src/FormManager/FormEdit/FormElementEdit/SequenceElementEdit.tsx b/packages/design/src/FormManager/FormEdit/FormElementEdit/SequenceElementEdit.tsx deleted file mode 100644 index 8a55f6de..00000000 --- a/packages/design/src/FormManager/FormEdit/FormElementEdit/SequenceElementEdit.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from 'react'; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; - -import { type FormDefinition, type FormElement } from '@atj/forms'; -import { type SequenceElement } from '@atj/forms/src/elements/sequence'; - -import RenderField from './RenderField'; -import { useFormContext } from 'react-hook-form'; - -interface ItemProps { - id: string; - form: FormDefinition; - element: FormElement; -} - -const SortableItem = ({ id, form, element }: ItemProps) => { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( - <li ref={setNodeRef} style={style}> - <div {...listeners} {...attributes} style={{ cursor: 'grab' }}> - ::: - </div> - <RenderField key={element.id} element={element} form={form} /> - </li> - ); -}; - -export const SequenceElementEdit = ({ - element, - form, -}: { - element: SequenceElement; - form: FormDefinition; -}) => { - const { register, setValue } = useFormContext(); - const [elements, setElements] = useState( - element.elements.map(elementId => { - return form.elements[elementId]; - }) - ); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) - ); - - return ( - <DndContext - sensors={sensors} - collisionDetection={closestCenter} - onDragEnd={event => { - const { active, over } = event; - if (over === null) { - return; - } - if (active.id !== over.id) { - const oldIndex = elements.findIndex(element => { - return element.id === active.id; - }); - const newIndex = elements.findIndex(element => { - return element.id === over.id; - }); - const newOrder = arrayMove(elements, oldIndex, newIndex); - setElements(newOrder); - setValue(element.id, { - id: element.id, - type: element.type, - elements: newOrder.map(element => element.id), - }); - } - }} - > - <SortableContext items={elements} strategy={verticalListSortingStrategy}> - <ul> - <input type="hidden" {...register(`${element.id}.id`)} /> - <input type="hidden" {...register(`${element.id}.type`)} /> - <input type="hidden" {...register(`${element.id}.elements`)} /> - {elements.map(elements => ( - <SortableItem - key={elements.id} - id={elements.id} - element={elements} - form={form} - /> - ))} - </ul> - </SortableContext> - </DndContext> - ); -}; diff --git a/packages/design/src/FormManager/FormEdit/index.tsx b/packages/design/src/FormManager/FormEdit/index.tsx index 8bd4cffc..3f915f46 100644 --- a/packages/design/src/FormManager/FormEdit/index.tsx +++ b/packages/design/src/FormManager/FormEdit/index.tsx @@ -10,12 +10,14 @@ import { updateElements, } from '@atj/forms'; -import RenderField from './FormElementEdit/RenderField'; +import { type FormUIContext } from '../../config'; export default function FormEdit({ + context, formId, formService, }: { + context: FormUIContext; formId: string; formService: FormService; }) { @@ -40,6 +42,7 @@ export default function FormEdit({ </li> </ul> <EditForm + context={context} form={form} onSave={form => formService.saveForm(formId, form)} /> @@ -47,54 +50,30 @@ export default function FormEdit({ ); } -// FIXME: Once we clean up the input type, this function should be unnecessary. -const getFormFieldMap = (elements: FormElementMap) => { - return Object.values(elements).reduce((acc, element) => { - if (element.type === 'input') { - acc[element.id] = { - type: 'input', - id: element.id, - text: element.text, - initial: element.initial.toString(), - required: element.required, - }; - return acc; - } else if (element.type === 'sequence') { - acc[element.id] = { - type: 'sequence', - id: element.id, - elements: element.elements, - }; - return acc; - } else { - const _exhaustiveCheck: never = element; - return _exhaustiveCheck; - } - }, {} as FormElementMap); -}; - const EditForm = ({ + context, form, onSave, }: { + context: FormUIContext; form: FormDefinition; onSave: (form: FormDefinition) => void; }) => { - const formElements: FormElementMap = getFormFieldMap(form.elements); const methods = useForm<FormElementMap>({ - defaultValues: formElements, + defaultValues: form.elements, }); const rootField = getRootFormElement(form); + const Component = context.components[rootField.type]; return ( <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(data => { - const updatedForm = updateElements(form, data); + const updatedForm = updateElements(context.config, form, data); onSave(updatedForm); })} > <ButtonBar /> - <RenderField form={form} element={rootField} /> + <Component context={context} form={form} element={rootField} /> <ButtonBar /> </form> </FormProvider> diff --git a/packages/design/src/FormManager/FormPreview/FormPreview.stories.tsx b/packages/design/src/FormManager/FormPreview/FormPreview.stories.tsx index 31d0eb7d..9c79a065 100644 --- a/packages/design/src/FormManager/FormPreview/FormPreview.stories.tsx +++ b/packages/design/src/FormManager/FormPreview/FormPreview.stories.tsx @@ -5,7 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { createTestFormService } from '@atj/form-service'; import { FormViewById } from '.'; -import { createTestForm } from '../../test-form'; +import { createTestForm, createTestFormConfig } from '../../test-form'; export default { title: 'FormManager/FormView', @@ -18,6 +18,7 @@ export default { ), ], args: { + config: createTestFormConfig(), formId: 'test-form', formService: createTestFormService({ 'test-form': createTestForm(), diff --git a/packages/design/src/FormManager/FormPreview/index.tsx b/packages/design/src/FormManager/FormPreview/index.tsx index 09e89d90..e18f7beb 100644 --- a/packages/design/src/FormManager/FormPreview/index.tsx +++ b/packages/design/src/FormManager/FormPreview/index.tsx @@ -1,12 +1,16 @@ import React from 'react'; +import { type FormConfig } from '@atj/forms'; import { type FormService } from '@atj/form-service'; + import Form from '../../Form'; export const FormViewById = ({ + config, formId, formService, }: { + config: FormConfig; formId: string; formService: FormService; }) => { @@ -18,6 +22,7 @@ export const FormViewById = ({ return ( <Form + config={config} form={result.data} onSubmit={async data => { const submission = await formService.submitForm(formId, data); diff --git a/packages/design/src/FormManager/import-document.tsx b/packages/design/src/FormManager/import-document.tsx index 7ca140cc..3a609969 100644 --- a/packages/design/src/FormManager/import-document.tsx +++ b/packages/design/src/FormManager/import-document.tsx @@ -1,14 +1,18 @@ import React from 'react'; + +import { FormConfig } from '@atj/forms'; import { type FormService } from '@atj/form-service'; import DocumentImporter from './DocumentImporter'; export const FormDocumentImport = ({ baseUrl, + config, formId, formService, }: { baseUrl: string; + config: FormConfig; formId: string; formService: FormService; }) => { @@ -19,6 +23,7 @@ export const FormDocumentImport = ({ } return ( <DocumentImporter + config={config} formService={formService} baseUrl={baseUrl} formId={formId} diff --git a/packages/design/src/FormManager/index.tsx b/packages/design/src/FormManager/index.tsx index f3492f39..075b42e3 100644 --- a/packages/design/src/FormManager/index.tsx +++ b/packages/design/src/FormManager/index.tsx @@ -8,11 +8,14 @@ import FormEdit from './FormEdit'; import FormList from './FormList'; import { FormViewById } from './FormPreview'; import { FormDocumentImport } from './import-document'; +import { FormUIContext } from '../config'; export default function FormManager({ + context, baseUrl, formService, }: { + context: FormUIContext; baseUrl: string; formService: FormService; }) { @@ -32,7 +35,13 @@ export default function FormManager({ if (formId === undefined) { return <div>formId is undefined</div>; } - return <FormViewById formId={formId} formService={formService} />; + return ( + <FormViewById + config={context.config} + formId={formId} + formService={formService} + /> + ); }} /> <Route @@ -42,7 +51,13 @@ export default function FormManager({ if (formId === undefined) { return <div>formId is undefined</div>; } - return <FormEdit formId={formId} formService={formService} />; + return ( + <FormEdit + context={context} + formId={formId} + formService={formService} + /> + ); }} /> <Route @@ -64,6 +79,7 @@ export default function FormManager({ } return ( <FormDocumentImport + config={context.config} baseUrl={baseUrl} formId={formId} formService={formService} diff --git a/packages/design/src/FormRouter/index.tsx b/packages/design/src/FormRouter/index.tsx index ceda5627..b59082a4 100644 --- a/packages/design/src/FormRouter/index.tsx +++ b/packages/design/src/FormRouter/index.tsx @@ -3,11 +3,14 @@ import { useParams, HashRouter, Route, Routes } from 'react-router-dom'; import { type FormService } from '@atj/form-service'; import Form from '../Form'; +import { type FormConfig } from '@atj/forms'; // Wrapper around Form that includes a client-side router for loading forms. export default function FormRouter({ + config, formService, }: { + config: FormConfig; formService: FormService; }) { return ( @@ -33,6 +36,7 @@ export default function FormRouter({ } return ( <Form + config={config} form={result.data} onSubmit={async data => { const submission = await formService.submitForm(formId, data); diff --git a/packages/design/src/config/InputElementEdit.tsx b/packages/design/src/config/InputElementEdit.tsx new file mode 100644 index 00000000..3e6a2e0c --- /dev/null +++ b/packages/design/src/config/InputElementEdit.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { type InputElement } from '@atj/forms/src/config/elements/input'; + +import { type FormElementComponent } from '.'; + +const InputElementEdit: FormElementComponent<InputElement> = ({ element }) => { + const { register } = useFormContext(); + return ( + <div className="grid-row grid-gap"> + <div className="grid-col"> + <label className="usa-label"> + Input type + <select className="usa-select" {...register(`${element.id}.type`)}> + <option value={'input'}>Input</option> + <option value={'textarea'}>Textarea</option> + </select> + </label> + </div> + <div className="grid-col"> + <label className="usa-label"> + Field label + <input + className="usa-input" + {...register(`${element.id}.data.text`)} + type="text" + ></input> + </label> + </div> + <div className="grid-col"> + <label className="usa-label"> + Default value + <input + className="usa-input" + type="text" + {...register(`${element.id}.data.initial`)} + ></input> + </label> + </div> + <div className="grid-col"> + <div className="usa-checkbox"> + <input + className="usa-checkbox__input" + type="checkbox" + id={`${element.id}.data.required`} + {...register(`${element.id}.data.required`)} + /> + <label + className="usa-checkbox__label" + htmlFor={`${element.id}.data.required`} + > + Required + </label> + </div> + </div> + </div> + ); +}; + +export default InputElementEdit; diff --git a/packages/design/src/config/SequenceElementEdit.tsx b/packages/design/src/config/SequenceElementEdit.tsx new file mode 100644 index 00000000..40846fef --- /dev/null +++ b/packages/design/src/config/SequenceElementEdit.tsx @@ -0,0 +1,127 @@ +import React, { useState } from 'react'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { useFormContext } from 'react-hook-form'; + +import { type FormDefinition, type FormElement } from '@atj/forms'; +import { type SequenceElement } from '@atj/forms/src/config/elements/sequence'; + +import { type FormElementComponent, type FormUIContext } from '.'; + +interface ItemProps<T> { + id: string; + form: FormDefinition; + element: FormElement<T>; + context: FormUIContext; +} + +const SortableItem = <T,>({ id, form, element, context }: ItemProps<T>) => { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const Component = context.components[element.type]; + + return ( + <li ref={setNodeRef} style={style}> + <div {...listeners} {...attributes} style={{ cursor: 'grab' }}> + ::: + </div> + <Component + key={element.id} + context={context} + element={element} + form={form} + /> + </li> + ); +}; + +const SequenceElementEdit: FormElementComponent<SequenceElement> = ({ + context, + form, + element, +}) => { + const { register, setValue } = useFormContext(); + const [elements, setElements] = useState( + element.data.elements.map(elementId => { + return form.elements[elementId]; + }) + ); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) + ); + + return ( + <fieldset> + <DndContext + sensors={sensors} + collisionDetection={closestCenter} + onDragEnd={event => { + const { active, over } = event; + if (over === null) { + return; + } + if (active.id !== over.id) { + const oldIndex = elements.findIndex(element => { + return element.id === active.id; + }); + const newIndex = elements.findIndex(element => { + return element.id === over.id; + }); + const newOrder = arrayMove(elements, oldIndex, newIndex); + setElements(newOrder); + setValue(element.id, { + id: element.id, + type: element.type, + data: { + elements: newOrder.map(element => element.id), + }, + } satisfies SequenceElement); + } + }} + > + <SortableContext + items={elements} + strategy={verticalListSortingStrategy} + > + <ul> + <input type="hidden" {...register(`${element.id}.id`)} /> + <input type="hidden" {...register(`${element.id}.type`)} /> + <input type="hidden" {...register(`${element.id}`)} /> + {elements.map(elements => ( + <SortableItem + key={elements.id} + id={elements.id} + context={context} + element={elements} + form={form} + /> + ))} + </ul> + </SortableContext> + </DndContext> + </fieldset> + ); +}; + +export default SequenceElementEdit; diff --git a/packages/design/src/config/index.ts b/packages/design/src/config/index.ts new file mode 100644 index 00000000..0e43e4c3 --- /dev/null +++ b/packages/design/src/config/index.ts @@ -0,0 +1,25 @@ +import React from 'react'; + +import { FormElement, type FormConfig, type FormDefinition } from '@atj/forms'; + +import InputElementEdit from './InputElementEdit'; +import SequenceElementEdit from './SequenceElementEdit'; + +export type FormUIContext = { + config: FormConfig; + components: ComponentForFormElement; +}; + +export type FormElementComponent<T extends FormElement> = React.ComponentType<{ + context: FormUIContext; + form: FormDefinition; + element: T; +}>; + +export type ComponentForFormElement<T extends FormElement = FormElement> = + Record<string, FormElementComponent<T>>; + +export const defaultFormElementComponent: ComponentForFormElement = { + sequence: SequenceElementEdit, + input: InputElementEdit, +}; diff --git a/packages/design/src/index.ts b/packages/design/src/index.ts index ed2bba96..463d805b 100644 --- a/packages/design/src/index.ts +++ b/packages/design/src/index.ts @@ -4,3 +4,4 @@ export { default as AvailableFormList } from './AvailableFormList'; export { default as Form } from './Form'; export { default as FormRouter } from './FormRouter'; export { default as FormManager } from './FormManager'; +export * from './config'; diff --git a/packages/design/src/test-form.ts b/packages/design/src/test-form.ts index 444014a1..94ad89cd 100644 --- a/packages/design/src/test-form.ts +++ b/packages/design/src/test-form.ts @@ -1,4 +1,6 @@ -import { createForm } from '@atj/forms'; +import { createForm, defaultFormConfig } from '@atj/forms'; + +import { defaultFormElementComponent } from './config'; export const createTestForm = () => { return createForm( @@ -12,23 +14,44 @@ export const createTestForm = () => { { type: 'sequence', id: 'root', - elements: ['element-1', 'element-2'], + data: { + elements: ['element-1', 'element-2'], + }, }, { type: 'input', id: 'element-1', - text: 'FormElement 1', - initial: '', - required: true, + data: { + text: 'FormElement 1', + required: true, + initial: '', + }, }, { type: 'input', id: 'element-2', - text: 'FormElement 2', - initial: 'initial value', - required: false, + data: { + text: 'FormElement 2', + required: false, + initial: 'test', + }, }, ], } ); }; + +export const createTestFormConfig = () => { + return defaultFormConfig; +}; + +export const createTestFormElementComponentMap = () => { + return defaultFormElementComponent; +}; + +export const createTestFormContext = () => { + return { + config: defaultFormConfig, + components: defaultFormElementComponent, + }; +}; diff --git a/packages/design/src/test-helper.ts b/packages/design/src/test-helper.ts index 29ced1c1..b4b0d339 100644 --- a/packages/design/src/test-helper.ts +++ b/packages/design/src/test-helper.ts @@ -1,7 +1,7 @@ -import { Store_CSFExports } from '@storybook/types'; -import { ReactRenderer, composeStories } from '@storybook/react'; +import { type Store_CSFExports } from '@storybook/types'; +import { type ReactRenderer, composeStories } from '@storybook/react'; import { render } from '@testing-library/react'; -import { Entries } from 'type-fest'; +import { type Entries } from 'type-fest'; import { describe, test } from 'vitest'; /** diff --git a/packages/documents/src/document.ts b/packages/documents/src/document.ts index e17439fa..c09874f6 100644 --- a/packages/documents/src/document.ts +++ b/packages/documents/src/document.ts @@ -40,39 +40,47 @@ export const addDocumentFieldsToForm = ( form: FormDefinition, fields: DocumentFieldMap ) => { - const elements: FormElement[] = []; + const elements: FormElement<any>[] = []; Object.entries(fields).map(([key, field]) => { if (field.type === 'CheckBox') { elements.push({ type: 'input', id: field.name, - text: field.label, - initial: field.value, - required: field.required, + data: { + text: field.label, + initial: field.value, + required: field.required, + }, }); } else if (field.type === 'OptionList') { elements.push({ type: 'input', id: field.name, - text: field.label, - initial: field.value, - required: field.required, + data: { + text: field.label, + initial: field.value, + required: field.required, + }, }); } else if (field.type === 'Dropdown') { elements.push({ type: 'input', id: field.name, - text: field.label, - initial: field.value, - required: field.required, + data: { + text: field.label, + initial: field.value, + required: field.required, + }, }); } else if (field.type === 'TextField') { elements.push({ type: 'input', id: field.name, - text: field.label, - initial: field.value, - required: field.required, + data: { + text: field.label, + initial: field.value, + required: field.required, + }, }); } else if (field.type === 'not-supported') { console.error(`Skipping field: ${field.error}`); @@ -83,7 +91,9 @@ export const addDocumentFieldsToForm = ( elements.push({ id: 'root', type: 'sequence', - elements: elements.map(element => element.id), + data: { + elements: elements.map(element => element.id), + }, }); return addFormElements(form, elements, 'root'); }; diff --git a/packages/form-service/src/operations/get-form.ts b/packages/form-service/src/operations/get-form.ts index 573bbafa..4c5c94b4 100644 --- a/packages/form-service/src/operations/get-form.ts +++ b/packages/form-service/src/operations/get-form.ts @@ -1,6 +1,6 @@ import { Result } from '@atj/common'; - import { type FormDefinition } from '@atj/forms'; + import { getFormFromStorage } from '../context/browser/form-repo'; export const getForm = ( diff --git a/packages/forms/src/config/config.ts b/packages/forms/src/config/config.ts new file mode 100644 index 00000000..d23a1d00 --- /dev/null +++ b/packages/forms/src/config/config.ts @@ -0,0 +1,13 @@ +import { type FormConfig } from '.'; +import { inputConfig } from './elements/input'; +import { sequenceConfig } from './elements/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: { + input: inputConfig, + sequence: sequenceConfig, + }, +}; diff --git a/packages/forms/src/config/elements/input.ts b/packages/forms/src/config/elements/input.ts new file mode 100644 index 00000000..b2bdd799 --- /dev/null +++ b/packages/forms/src/config/elements/input.ts @@ -0,0 +1,35 @@ +import { type FormElementConfig } from '..'; +import { type FormElement } from '../../elements'; +import { PromptPart } from '../../prompts'; + +export type InputElement = FormElement<{ + text: string; + initial: string; + required: boolean; +}>; + +export const inputConfig: FormElementConfig<InputElement> = { + initial: { + text: '', + initial: '', + required: true, + }, + parseData(obj: any) { + // TODO: runtime validation + return obj; + }, + getChildren() { + return []; + }, + createPrompt(_, session, element): PromptPart[] { + return [ + { + type: 'text' as const, + id: element.id, + value: session.data.values[element.id], + label: element.data.text, + required: element.data.required, + }, + ]; + }, +}; diff --git a/packages/forms/src/config/elements/sequence.ts b/packages/forms/src/config/elements/sequence.ts new file mode 100644 index 00000000..3629989f --- /dev/null +++ b/packages/forms/src/config/elements/sequence.ts @@ -0,0 +1,27 @@ +import { type FormElementConfig } from '..'; +import { type FormElement, type FormElementId } from '../../elements'; +import { type PromptPart, createPromptForElement } from '../../prompts'; + +export type SequenceElement = FormElement<{ + elements: FormElementId[]; +}>; + +export const sequenceConfig: FormElementConfig<SequenceElement> = { + initial: { + elements: [], + }, + parseData(obj) { + return obj; + }, + getChildren(element, elements) { + return element.data.elements.map( + (elementId: string) => elements[elementId] + ); + }, + createPrompt(config, session, element): PromptPart[] { + return element.data.elements.flatMap((elementId: string) => { + const element = session.form.elements[elementId]; + return createPromptForElement(config, session, element); + }); + }, +}; diff --git a/packages/forms/src/config/index.ts b/packages/forms/src/config/index.ts index 88415406..4d954854 100644 --- a/packages/forms/src/config/index.ts +++ b/packages/forms/src/config/index.ts @@ -1,15 +1,21 @@ -import { PromptPart } from '../prompts'; +import { type FormElement, type FormElementId } from '../elements'; +import { type CreatePrompt } from '../prompts'; -type FormConfigContext = {}; +export { defaultFormConfig } from './config'; -type FormElement<Schema extends { type: string }> = { - parseData: (data: Schema) => Schema | Error; - createPrompt?: (session: any, data: Schema) => PromptPart[]; // if a terminal element??? +export type FormElementConfig<ThisFormElement extends FormElement<any>> = { + initial: ThisFormElement['data']; + parseData: (obj: any) => ThisFormElement; + getChildren: ( + element: ThisFormElement, + elements: Record<FormElementId, FormElement<any>> + ) => FormElement<any>[]; + createPrompt: CreatePrompt<ThisFormElement>; }; - -export const createFormConfig = (context: FormConfigContext) => { - // or should this be createFormManager? +export type FormConfig<T extends FormElement<any> = FormElement<any>> = { + elements: Record<string, FormElementConfig<T>>; }; -// don't design this backwards. might want to start at createPrompt, and add -// in the bits that are actually required. +export type ConfigElements<Config extends FormConfig> = ReturnType< + Config['elements'][keyof Config['elements']]['parseData'] +>; diff --git a/packages/forms/src/elements.ts b/packages/forms/src/elements.ts new file mode 100644 index 00000000..1b183262 --- /dev/null +++ b/packages/forms/src/elements.ts @@ -0,0 +1,24 @@ +import { FormDefinition } from '..'; + +export type FormElement<T = any> = { + type: string; + id: FormElementId; + data: T; +}; + +export type FormElementId = string; +export type FormElementValue = any; +export type FormElementValueMap = Record<FormElementId, FormElementValue>; +export type FormElementMap = Record<FormElementId, FormElement<any>>; +export type GetFormElement = ( + form: FormDefinition, + id: FormElementId +) => FormElement<any>; + +export const getFormElementMap = (elements: FormElement<any>[]) => { + return Object.fromEntries( + elements.map(element => { + return [element.id, element]; + }) + ); +}; diff --git a/packages/forms/src/elements/index.ts b/packages/forms/src/elements/index.ts deleted file mode 100644 index 905750d8..00000000 --- a/packages/forms/src/elements/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { InputElement } from './input'; -import { SequenceElement } from './sequence'; - -// The collection of all form elements is a discriminated union. -// We may want the user of this library to be able to inject their own element -// types, but for now, we will just hardcode this type. -export type FormElement = InputElement | SequenceElement; - -export type FormElementId = string; -export type FormElementValue = any; -export type FormElementValueMap = Record<FormElementId, FormElementValue>; -export type FormElementMap = Record<FormElementId, FormElement>; - -export const getFormElementMap = (elements: FormElement[]) => { - return Object.fromEntries( - elements.map(element => { - return [element.id, element]; - }) - ); -}; diff --git a/packages/forms/src/elements/input.ts b/packages/forms/src/elements/input.ts deleted file mode 100644 index 0b05c113..00000000 --- a/packages/forms/src/elements/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FormElementId } from '.'; - -export type InputElement = { - type: 'input'; - id: FormElementId; - text: string; - initial: string | boolean | string[]; // TODO: create separate types - required: boolean; -}; diff --git a/packages/forms/src/elements/sequence.ts b/packages/forms/src/elements/sequence.ts deleted file mode 100644 index cd963c02..00000000 --- a/packages/forms/src/elements/sequence.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FormElementId } from '.'; - -export type SequenceElement = { - id: FormElementId; - type: 'sequence'; - elements: FormElementId[]; -}; diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index f0e87bcd..7686746c 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -1,13 +1,16 @@ +import { FormConfig } from './config'; +import { SequenceElement } from './config/elements/sequence'; import { type DocumentFieldMap } from './documents'; import { - getFormElementMap, - type FormElement, type FormElementId, + type FormElementMap, type FormElementValue, type FormElementValueMap, - type FormElementMap, + getFormElementMap, + FormElement, } from './elements'; +export * from './config'; export * from './documents'; export * from './elements'; export * from './prompts'; @@ -43,32 +46,25 @@ type FormOutput = { export const createForm = ( summary: FormSummary, initial: { - elements: FormElement[]; + elements: FormElement<any>[]; root: FormElementId; } = { elements: [ { id: 'root', type: 'sequence', - elements: [], - }, + data: { + elements: [], + }, + } satisfies SequenceElement, ], root: 'root', } ): FormDefinition => { return { summary, - root: 'root', - elements: { - root: { - id: 'root', - type: 'sequence', - elements: initial.elements.map(element => { - return element.id; - }), - }, - ...getFormElementMap(initial.elements), - }, + root: initial.root, + elements: getFormElementMap(initial.elements), outputs: [], }; }; @@ -77,24 +73,13 @@ export const getRootFormElement = (form: FormDefinition) => { return form.elements[form.root]; }; -const initialValueForFormElement = (element: FormElement) => { - if (element.type === 'input') { - return element.initial; - } else if (element.type === 'sequence') { - return []; - } else { - const _exhaustiveCheck: never = element; - return _exhaustiveCheck; - } -}; - export const createFormSession = (form: FormDefinition): FormSession => { return { data: { errors: {}, values: Object.fromEntries( Object.values(form.elements).map(element => { - return [element.id, initialValueForFormElement(element)]; + return [element.id, form.elements[element.id].data.initial]; }) ), }, @@ -114,7 +99,7 @@ export const updateForm = ( const nextForm = addValue(context, id, value); const element = context.form.elements[id]; if (element.type === 'input') { - if (element.required && !value) { + if (element.data.required && !value) { return addError(nextForm, id, 'Required value not provided.'); } } @@ -153,7 +138,7 @@ const addError = ( export const addFormElements = ( form: FormDefinition, - elements: FormElement[], + elements: FormElement<any>[], root?: FormElementId ) => { const formElementMap = getFormElementMap(elements); @@ -166,7 +151,7 @@ export const addFormElements = ( export const replaceFormElements = ( form: FormDefinition, - elements: FormElement[] + elements: FormElement<any>[] ): FormDefinition => { return { ...form, @@ -175,12 +160,13 @@ export const replaceFormElements = ( acc[element.id] = element; return acc; }, - {} as Record<FormElementId, FormElement> + {} as Record<FormElementId, FormElement<any>> ), }; }; export const updateElements = ( + config: FormConfig, form: FormDefinition, newElements: FormElementMap ): FormDefinition => { @@ -188,35 +174,17 @@ export const updateElements = ( const targetElements: FormElementMap = { root, }; - contributeElements(targetElements, newElements, 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)); + return { ...form, elements: targetElements, }; }; -// Contribute a FormElement and all its children to a FormElementMap. -// This function may be used to create a minimal map of required fields. -const contributeElements = ( - target: FormElementMap, - source: FormElementMap, - element: FormElement -): FormElementMap => { - if (element.type === 'input') { - target[element.id] = element; - return target; - } else if (element.type === 'sequence') { - element.elements.forEach(elementId => { - const sequenceElement = source[elementId]; - return contributeElements(target, source, sequenceElement); - }); - return target; - } else { - const _exhaustiveCheck: never = element; - return _exhaustiveCheck; - } -}; - export const addFormOutput = (form: FormDefinition, document: FormOutput) => { return { ...form, diff --git a/packages/forms/src/prompts/index.ts b/packages/forms/src/prompts.ts similarity index 55% rename from packages/forms/src/prompts/index.ts rename to packages/forms/src/prompts.ts index 1404eb9a..635eb770 100644 --- a/packages/forms/src/prompts/index.ts +++ b/packages/forms/src/prompts.ts @@ -1,6 +1,11 @@ // For now, a prompt just returns an array of elements. This will likely need -import { getRootFormElement, type FormSession, type FormElement } from '..'; +import { + getRootFormElement, + type FormSession, + type FormElement, + FormConfig, +} from '..'; export type TextInputPrompt = { type: 'text'; @@ -30,7 +35,10 @@ export type Prompt = { }; // to be filled out to support more complicated display formats. -export const createPrompt = (session: FormSession): Prompt => { +export const createPrompt = ( + config: FormConfig, + session: FormSession +): Prompt => { const parts: PromptPart[] = [ { type: 'form-summary', @@ -39,7 +47,7 @@ export const createPrompt = (session: FormSession): Prompt => { }, ]; const root = getRootFormElement(session.form); - parts.push(...createPromptForElement(session, root)); + parts.push(...createPromptForElement(config, session, root)); return { actions: [ { @@ -51,27 +59,16 @@ export const createPrompt = (session: FormSession): Prompt => { }; }; -const createPromptForElement = ( +export type CreatePrompt<T> = ( + config: FormConfig, session: FormSession, - element: FormElement -): PromptPart[] => { - if (element.type === 'input') { - return [ - { - type: 'text' as const, - id: element.id, - value: session.data.values[element.id], - label: element.text, - required: element.required, - }, - ]; - } else if (element.type === 'sequence') { - return element.elements.flatMap(elementId => { - const element = session.form.elements[elementId]; - return createPromptForElement(session, element); - }); - } else { - const _exhaustiveCheck: never = element; - return _exhaustiveCheck; - } + element: T +) => PromptPart[]; + +export const createPromptForElement: CreatePrompt<FormElement<any>> = ( + config, + session, + element +) => { + return config.elements[element.type].createPrompt(config, session, element); }; diff --git a/packages/forms/tests/two-field-form.test.ts b/packages/forms/tests/two-field-form.test.ts index 189928af..259144ff 100644 --- a/packages/forms/tests/two-field-form.test.ts +++ b/packages/forms/tests/two-field-form.test.ts @@ -3,25 +3,31 @@ import { describe, expect, test } from 'vitest'; import * as forms from '../src'; import { createPrompt } from '../src'; -const elements: forms.FormElement[] = [ +const elements: forms.FormElement<any>[] = [ { type: 'sequence', id: 'root', - elements: ['element-1', 'element-2'], + data: { + elements: ['element-1', 'element-2'], + }, }, { type: 'input', id: 'element-1', - text: 'What is your first name?', - initial: '', - required: true, + data: { + text: 'What is your first name?', + initial: '', + required: true, + }, }, { type: 'input', id: 'element-2', - text: 'What is your favorite word?', - initial: '', - required: false, + data: { + text: 'What is your favorite word?', + initial: '', + required: false, + }, }, ]; const form = forms.createForm( @@ -123,9 +129,10 @@ describe('two element form session', () => { }); describe('two element prompt', () => { + const config = forms.defaultFormConfig; const session = forms.createFormSession(form); test('includes a submit button', () => { - const prompt = createPrompt(session); + const prompt = createPrompt(config, session); expect(prompt.actions).toEqual([ { type: 'submit', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e99f585..f8d40647 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@types/node': specifier: ^20.11.16 version: 20.11.16 + '@vitest/coverage-c8': + specifier: ^0.33.0 + version: 0.33.0(vitest@0.34.6) '@vitest/coverage-v8': specifier: ^1.2.2 version: 1.2.2(vitest@0.34.6) @@ -3246,18 +3249,18 @@ packages: exit: 0.1.2 glob: 7.2.3 graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.1 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 + istanbul-reports: 3.1.6 jest-message-util: 29.7.0 jest-util: 29.7.0 jest-worker: 29.7.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - v8-to-istanbul: 9.1.0 + v8-to-istanbul: 9.2.0 transitivePeerDependencies: - supports-color dev: true @@ -4924,7 +4927,7 @@ packages: express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.1.1 - magic-string: 0.30.3 + magic-string: 0.30.7 rollup: 3.29.1 typescript: 5.3.3 vite: 5.0.12(@types/node@20.11.16) @@ -6230,6 +6233,20 @@ packages: transitivePeerDependencies: - supports-color + /@vitest/coverage-c8@0.33.0(vitest@0.34.6): + resolution: {integrity: sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==} + deprecated: v8 coverage is moved to @vitest/coverage-v8 package + peerDependencies: + vitest: '>=0.30.0 <1' + dependencies: + '@ampproject/remapping': 2.2.1 + c8: 7.14.0 + magic-string: 0.30.7 + picocolors: 1.0.0 + std-env: 3.7.0 + vitest: 0.34.6(@vitest/ui@1.2.2) + dev: true + /@vitest/coverage-v8@0.34.6(vitest@0.34.6): resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} peerDependencies: @@ -7608,6 +7625,25 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + /c8@7.14.0: + resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.1.6 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.9 + dev: true + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -8030,7 +8066,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: false /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -8984,7 +9019,7 @@ packages: dependencies: semver: 7.5.4 shelljs: 0.8.5 - typescript: 5.4.0-dev.20240206 + typescript: 5.4.0-dev.20240209 dev: false /dset@3.1.3: @@ -11736,7 +11771,7 @@ packages: dependencies: '@babel/core': 7.23.7 '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -11749,7 +11784,7 @@ packages: '@babel/core': 7.23.7 '@babel/parser': 7.23.6 '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -11774,7 +11809,7 @@ packages: dependencies: archy: 1.0.0 cross-spawn: 7.0.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 p-map: 3.0.0 rimraf: 3.0.2 uuid: 8.3.2 @@ -14185,13 +14220,13 @@ packages: foreground-child: 2.0.0 get-package-type: 0.1.0 glob: 7.2.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 istanbul-lib-hook: 3.0.0 istanbul-lib-instrument: 4.0.3 istanbul-lib-processinfo: 2.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 + istanbul-reports: 3.1.6 make-dir: 3.1.0 node-preload: 0.2.1 p-map: 3.0.0 @@ -17752,8 +17787,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - /typescript@5.4.0-dev.20240206: - resolution: {integrity: sha512-8P1XYxDbG/AyGE5tB8+JpeiQfS5ye1BTvIVDZaHhoK9nJuCn4nkB0L66lvfwYB+46hA4rLo3vE3WkIToSYtqQA==} + /typescript@5.4.0-dev.20240209: + resolution: {integrity: sha512-qhstJB8bOkf9hDabhxluOD3J94iUqsQZtTcia/T8ymwqh2ziTXx9z9OQg3sxJOcUEVVxwAxKybCiwLM2HxwPwg==} engines: {node: '>=14.17'} hasBin: true dev: false @@ -19088,7 +19123,6 @@ packages: /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - dev: false /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} @@ -19128,7 +19162,6 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - dev: false /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}