From cd800fdb75bc8e2548f8bb4f3cf33ad5469f483d Mon Sep 17 00:00:00 2001 From: Josue Ruiz <7465495+SwaySway@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:10:42 -0700 Subject: [PATCH] feat/merge-forms-and-views (#648) * chore: add form-related types (#469) Co-authored-by: Hein Jeong * chore: add form definition mapper (#470) Co-authored-by: Hein Jeong * test: update cypress setup (#472) * chore: update cli setup flow and update react peerDep (#473) * feat: adding string format types in lib * feat: add base form to component mapper with updated datastore types * chore: audit fix and peerDep of datastore * chore: update deps in lock files * feat: add base form renderer and amplify form renderer classes * chore: update form definition mapper (#489) Co-authored-by: Hein Jeong * feat: string formatter util functions * feat: view types and string formatting on view config * fix: optional enum type and nullish handling * chore: map input and sectional form elements (#493) Co-authored-by: Hein Jeong * feat: adding types for Slot * fix: rename Slot to Amplify.Slot * chore: add util to map datastore schema to generic (#498) Co-authored-by: Hein Jeong * feat: support slot binding (#499) Co-authored-by: Hein Jeong * chore: skip field generation for read-only fields (#501) * chore: skip field generation for read-only fields * chore: skip field generation for read-only fields * chore: skip field generation for read-only fields * chore: skip field generation for read-only fields * chore: skip field generation for read-only fields Co-authored-by: Scott Young * chore: add validation field types for forms (#502) Co-authored-by: Justin Shih * feat: expand return type of overrideItems (#504) * chore: clean up form definition mapper (#508) Co-authored-by: Hein Jeong * chore: add form field validation function and additional validation types (#507) Co-authored-by: Justin Shih * chore: rev recommended version of ui-react (#513) Co-authored-by: Hein Jeong * feat: add nested query if data schema is passed down * chore/main merge (#516) * chore(release): update package with security issue * fix: handle auth prop in concat * chore(release): security issues in dependencies Co-authored-by: Kevin Pranoto * chore: update mocked to use jest.mocked * feat: add react-studio-form-renderer and update test ci (#505) * chore: update primitive test for SliderField * feat: add datastore onSubmitBefore and onSubmitComplete callbacks (#526) * feat: add datastore onSubmitBefore and onSubmitComplete callbacks * feat: add datastore onSubmitBefore and onSubmitComplete callbacks * feat: add datastore onSubmitBefore and onSubmitComplete callbacks * feat: add datastore onSubmitBefore and onSubmitComplete callbacks * feat: add datastore onSubmitBefore and onSubmitComplete callbacks Co-authored-by: Scott Young Co-authored-by: Justin Shih * chore: take GenericDataSchema into form renderer and definition mapper (#528) Co-authored-by: Hein Jeong * Render form utils and add forms to index file (#536) * fix: remove locale dependency on date formatting * feat: add utils file renderer Co-authored-by: Kevin Pranoto Co-authored-by: Justin Shih * chore: add valueMappings to forms (#537) Co-authored-by: Hein Jeong * feat: add onchange handling for form renderer (#542) Co-authored-by: Justin Shih * chore: change default value mappings (#543) Co-authored-by: Hein Jeong * chore: add id to StudioForm (#544) Co-authored-by: Justin Shih * chore: update pagination label in test (#547) Co-authored-by: Justin Shih * fix: change props of SwitchField form element (#546) Co-authored-by: Hein Jeong * chore: update view types to use string primitives (#535) * chore: update validation type to match model (#545) Co-authored-by: Justin Shih * chore: update pagination label in test (#547) Co-authored-by: Justin Shih * chore: tidying forms renderer (#549) Co-authored-by: Justin Shih * chore: update react import & updated factory methods for import collection (#551) * fix: onSubmit for custom forms (#552) * feat: make form labels use sentence case * chore: update render component only (#558) Updates - this will update the import for 'validateField' to come from codegen-ui-react - update view type to StudioView - update validation to be rendered during component rendering * feat: table definition for renderer * chore: adds update datastore action for forms (#559) Co-authored-by: Justin Shih * feat: support input types (#557) Co-authored-by: Hein Jeong * chore: update pkg lock (#562) updating the dependency changes noted in the package-lock diff * chore: separate out and export logic to map model field to field config (#566) Co-authored-by: Hein Jeong * feat: add default, unremovable validations for certain form inputs (#564) Co-authored-by: Hein Jeong * fix: update label to children for cta buttons on forms update form-to-component ctaConfig to use children as opposed to labels to render the button text * chore: only generate breakpoint mapping on breakpoint variants (#569) * fix: fix to narrow view config type * chore: move form-to-component util (#573) Co-authored-by: Hein Jeong * feat: initial commit for view (table) renderer * chore: pkg lock pointing to new released ver in main * feat: add override types to form elements (#576) Co-authored-by: Justin Shih * feat: table renderer * feat: render options for SelectField and RadioGroupField (#577) Co-authored-by: Hein Jeong * chore: update override props snapshot * feat: view renderer with variable statements and base types * chore: make appId and environmentName optional for StudioView (#582) Co-authored-by: Hein Jeong * fix: codegenning formatter in table jsx * chore: fix form types (#583) Co-authored-by: Justin Shih * feat: handle custom json model for tables * fix: conditionally codegenning formatter call * fix: add type casting when doing on change * feat: formatter init * fix: expose amplfyViewRenderer and update settings for passing (#591) predicates * fix: type mismatch fix with model * chore: refine rendering of form sectional elements (#594) Co-authored-by: Hein Jeong * chore: update form sectional element type (#596) Co-authored-by: Hein Jeong * chore: change form validation (#597) Co-authored-by: Hein Jeong * feat: support for cta labels (#548) * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * Tagged release/forms and views (#555) * chore: update view types to use string primitives (#535) * chore: update validation type to match model (#545) Co-authored-by: Justin Shih * chore: tidying forms renderer (#549) Co-authored-by: Justin Shih * chore: update react import & updated factory methods for import collection (#551) Co-authored-by: awinberg-aws <100880084+awinberg-aws@users.noreply.github.com> Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: Josue Ruiz <7465495+SwaySway@users.noreply.github.com> * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * Tagged release/forms and views (#561) * chore: adds update datastore action for forms (#559) Co-authored-by: Justin Shih * feat: support input types (#557) Co-authored-by: Hein Jeong Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: Hein Jeong <73264629+hein-j@users.noreply.github.com> Co-authored-by: Hein Jeong * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * Tagged release/forms and views (#580) * feat: add override types to form elements (#576) Co-authored-by: Justin Shih * feat: table renderer * feat: render options for SelectField and RadioGroupField (#577) Co-authored-by: Hein Jeong Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: Kevin Pranoto Co-authored-by: Hein Jeong <73264629+hein-j@users.noreply.github.com> Co-authored-by: Hein Jeong * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels * feat: support for cta labels Co-authored-by: Scott Young Co-authored-by: awinberg-aws <100880084+awinberg-aws@users.noreply.github.com> Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: Josue Ruiz <7465495+SwaySway@users.noreply.github.com> Co-authored-by: Hein Jeong <73264629+hein-j@users.noreply.github.com> Co-authored-by: Hein Jeong Co-authored-by: Kevin Pranoto * chore: change form validation, validate on submit, and init form integ test (#600) Co-authored-by: Hein Jeong * fix: render cta without overrides & add cta to form integ test model (#603) Co-authored-by: Hein Jeong * chore: change form cta visible property to excluded (#604) Co-authored-by: Hein Jeong * fix: entity name validation * chore: add model prop to update form (#601) Co-authored-by: Justin Shih * fix: remove @aws-amplify/codegen-ui-react as import source By removing this import generated forms can now use the utils file for validateField use. This shouldn't be an issue for UI as that does not render the import source for the files and we can decide the import value in the UI code. * fix: remove pascal case from themes as the default theme name is "studioTheme" * fix: add optional chaining for cta config and nullish coalese * fix: date format and form onSubmitBefore * fix: fix release yml (#571) Co-authored-by: Justin Shih * feat: allow conditional formatting of outputs * feat: add array field component type (#613) Co-authored-by: Justin Shih * chore: handle RadioGroupField of Boolean type (#610) Co-authored-by: Hein Jeong * chore: add phone validation, change required validation, and fix regex (#615) Co-authored-by: Hein Jeong * fix: do not assume related model or field is in datastore schema (#621) Co-authored-by: Hein Jeong * chore: clean up form auto gen (#618) Co-authored-by: Hein Jeong * fix: make override keys optional (#622) Co-authored-by: Justin Shih * fix: add cta to yup validation * feat: add support for nested json (#623) * fix: fetch table config with nullish operator * chore: use ts versions below 4.5 (#626) * feat: add custom ArrayField component to form renderer (#624) * feat: add custom ArrayField component to form renderer * feat: add support for nested json (#623) * fix: merge conflicts for Array field component render Co-authored-by: Justin Shih * fix: handle modelFields for onSubmitBefore (#629) * feat: add event types to form override props (#627) Co-authored-by: Justin Shih * chore: handle bad form element positions (#619) Co-authored-by: Hein Jeong * fix: change util file rendering (#630) Co-authored-by: Hein Jeong * fix: add default values for update form (#631) Co-authored-by: Justin Shih * fix: test all configured validations for field (#632) * chore: remove unnecessary grids (#633) * chore: validate onBlur and only conditionally onChange (#635) Co-authored-by: Hein Jeong * fix: add padding properly to forms (#637) Co-authored-by: Hein Jeong * fix: change form hook names (#636) * fix: add array fields for autogenerated forms (#634) Co-authored-by: Justin Shih * fix: update fix onSubmit types for ds and custom (#638) * chore: remove top margin from CTA row (#639) Co-authored-by: Hein Jeong * feat: add onChange override function (#640) * fix: update form useEffect to conditionally change state if record is defined (#641) * fix: add inputType for form hook functions (#642) * chore: refine types and onChange handlers (#643) Co-authored-by: Hein Jeong * fix: add arrayfield component to componentOnly form renderer (#644) Co-authored-by: Justin Shih * fix: include minimum breakpoints required (#645) * Chore/merge main (#646) * fix: fix release yml (#571) Co-authored-by: Justin Shih * feat: allow conditional formatting of outputs Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: Christopher Woolum * fix: add state to controlled components (#649) * fix: onChange overrides to use onchange value and add useEffect initial data for update custom forms (#650) * fix: increase commit max length * fix: forms row layout (#653) * fix: remove ArrayField items state and change onBlur to validate input field value (#654) Co-authored-by: Justin Shih * fix: onClear to reset state values and errors (#655) * fix: remove async from onblur (#656) * feat: take design tokens into consideration when rendering * feat: take design tokens into consideration when rendering Co-authored-by: Hein Jeong <73264629+hein-j@users.noreply.github.com> Co-authored-by: Hein Jeong Co-authored-by: Kevin Pranoto Co-authored-by: Scott Young Co-authored-by: Scott Young Co-authored-by: Justin Shih <36183898+Jshhhh@users.noreply.github.com> Co-authored-by: Justin Shih Co-authored-by: awinberg-aws <100880084+awinberg-aws@users.noreply.github.com> Co-authored-by: Christopher Woolum Co-authored-by: Chris Woolum Co-authored-by: felipechiave <107582550+felipechiave@users.noreply.github.com> Co-authored-by: David Lopez Co-authored-by: David Lopez --- .circleci/config.yml | 2 +- CHANGELOG.md | 24 +- commitlint.config.js | 1 + jest.config.js | 2 +- packages/codegen-ui-react/CHANGELOG.md | 24 +- ...udio-template-renderer-helper.test.ts.snap | 18 +- ...studio-ui-codegen-react-forms.test.ts.snap | 3612 +++++++++++++++++ ...studio-ui-codegen-react-views.test.ts.snap | 378 ++ .../studio-ui-codegen-react.test.ts.snap | 367 +- .../__utils__/amplify-renderer-generator.ts | 90 +- .../__snapshots__/form-state.test.ts.snap | 9 + .../__snapshots__/type-helper.test.ts.snap | 26 + .../lib/__tests__/forms/form-state.test.ts | 59 + .../lib/__tests__/forms/type-helper.test.ts | 72 + .../lib/__tests__/forms/validation.test.ts | 228 +- .../import-collection.test.ts.snap | 16 +- .../form-renderer-helper.test.ts.snap | 21 + .../react-forms/form-renderer-helper.test.ts | 63 + .../studio-ui-codegen-react-forms.test.ts | 107 + .../studio-ui-codegen-react-views.test.ts | 56 + .../__tests__/studio-ui-codegen-react.test.ts | 14 +- .../string-formatter.test.ts.snap | 103 + .../lib/__tests__/utils/fetch-by-path.test.ts | 39 + .../__tests__/utils/string-formatter.test.ts | 23 + .../amplify-form-renderer.ts | 150 +- .../amplify-view-renderer.ts | 36 + .../lib/amplify-ui-renderers/form.ts | 136 +- .../lib/forms/event-targets.ts | 126 + .../lib/forms/form-renderer-helper.ts | 1227 ++++++ .../codegen-ui-react/lib/forms/form-state.ts | 255 ++ packages/codegen-ui-react/lib/forms/index.ts | 18 + .../lib/forms/react-form-renderer.ts | 463 +++ .../codegen-ui-react/lib/forms/type-helper.ts | 452 +++ .../lib/forms/typescript-type-map.ts | 31 + .../codegen-ui-react/lib/helpers/index.ts | 16 + .../lib/imports/import-collection.ts | 16 +- .../lib/imports/import-mapping.ts | 8 + packages/codegen-ui-react/lib/index.ts | 6 + .../lib/react-component-render-helper.ts | 36 + .../lib/react-component-renderer.ts | 15 + .../react-index-studio-template-renderer.ts | 4 +- .../react-studio-template-renderer-helper.ts | 48 + .../lib/react-studio-template-renderer.ts | 128 +- .../lib/react-table-renderer-helper.ts | 133 + .../lib/react-table-renderer.ts | 320 ++ .../react-utils-studio-template-renderer.ts | 107 + .../lib/utils/forms/array-field-component.ts | 844 ++++ .../lib/utils/forms/layout-helpers.ts | 25 + .../lib/utils/forms/validation.ts | 1739 +++++++- .../lib/utils/generate-react-hooks.ts | 35 + .../lib/utils/json-path-fetch.ts | 232 ++ .../lib/utils/string-formatter.ts | 968 +++++ .../lib/views/react-view-renderer.ts | 479 +++ packages/codegen-ui-react/package-lock.json | 4 +- packages/codegen-ui-react/package.json | 2 +- packages/codegen-ui/CHANGELOG.md | 21 +- .../componentWithBreakpoint.json | 33 + .../datastore/input-gallery.json | 123 + .../example-schemas/datastore/person.json | 48 + .../example-schemas/datastore/post-ds.json | 106 + .../example-schemas/datastore/post.json | 89 + .../forms/bio-nested-create.json | 63 + .../forms/bio-nested-update.json | 59 + .../forms/custom-with-sectional-elements.json | 44 + .../forms/input-gallery-create.json | 35 + .../forms/input-gallery-update.json | 35 + .../forms/post-custom-create.json | 66 + .../forms/post-custom-update.json | 66 + .../forms/post-datastore-create-row.json | 77 + .../forms/post-datastore-create.json | 17 + .../forms/post-datastore-update.json | 47 + .../views/post-table-custom-format.json | 45 + .../views/post-table-datastore.json | 32 + .../views/table-from-custom-json.json | 22 + .../views/table-from-datastore-no-header.json | 38 + .../views/table-from-datastore.json | 61 + packages/codegen-ui/index.ts | 1 + .../__utils__/basic-form-definition.ts | 33 + .../lib/__tests__/__utils__/mock-schemas.ts | 89 + .../form-to-component.test.ts | 48 - .../generate-form-definition.test.ts | 185 +- .../helpers/datastore-model.test.ts | 131 - .../helpers/form-field.test.ts | 349 +- .../helpers/map-elements.test.ts | 16 +- .../helpers/model-fields-configs.test.ts | 305 ++ .../helpers/position.test.ts | 81 +- .../helpers/sectional-element.test.ts | 12 +- .../generate-table-definition.test.ts | 177 + .../order-filter-columns.test.ts | 120 + .../__tests__/generic-from-datastore.test.ts | 24 +- .../lib/__tests__/string-formatter.test.ts | 78 +- .../utils/form-component-metadata.test.ts | 103 + .../map-form-definition-to-component.test.ts | 91 + .../form-to-component.ts | 210 - .../generate-form-definition.ts | 35 +- .../helpers/datastore-model.ts | 60 - .../helpers/defaults.ts | 19 +- .../helpers/field-type-map.ts | 52 +- .../helpers/form-field.ts | 200 +- .../generate-form-definition/helpers/index.ts | 3 +- .../helpers/map-cta.ts | 77 + .../helpers/map-styles.ts | 8 +- .../helpers/model-fields-configs.ts | 121 + .../helpers/position.ts | 111 +- .../lib/generate-form-definition/index.ts | 9 +- .../generate-table-definition.ts | 85 + .../generate-view-definition/helpers/index.ts | 16 + .../helpers/order-filter-columns.ts | 130 + .../lib/generate-view-definition/index.ts | 16 + .../codegen-ui/lib/generic-from-datastore.ts | 7 +- packages/codegen-ui/lib/renderer-helper.ts | 12 + packages/codegen-ui/lib/types/data.ts | 9 +- packages/codegen-ui/lib/types/form/fields.ts | 2 + .../codegen-ui/lib/types/form/form-cta.ts | 38 + .../lib/types/form/form-definition-element.ts | 50 +- .../lib/types/form/form-definition.ts | 38 +- .../lib/types/form/form-metadata.ts | 41 + .../lib/types/form/form-validation.ts | 48 +- packages/codegen-ui/lib/types/form/index.ts | 52 +- .../codegen-ui/lib/types/form/input-config.ts | 24 +- .../lib/types/form/sectional-element.ts | 2 +- packages/codegen-ui/lib/types/form/style.ts | 9 +- packages/codegen-ui/lib/types/index.ts | 7 + .../codegen-ui/lib/types/string-format.ts | 22 +- .../codegen-ui/lib/types/view/defaults.ts | 65 + packages/codegen-ui/lib/types/view/index.ts | 3 + packages/codegen-ui/lib/types/view/style.ts | 6 +- .../lib/types/view/table-definition.ts | 30 + packages/codegen-ui/lib/types/view/table.ts | 2 +- .../lib/types/view/view-metadata.ts | 24 + packages/codegen-ui/lib/types/view/view.ts | 36 +- .../codegen-ui/lib/utils/breakpoint-utils.ts | 49 + .../lib/utils/component-metadata.ts | 2 + .../lib/utils/form-component-metadata.ts | 79 + .../helpers/map-cta-buttons.ts | 113 + .../helpers/map-element-children.ts | 60 + .../lib/utils/form-to-component/index.ts | 16 + .../map-form-definition-to-component.ts | 98 + packages/codegen-ui/lib/utils/index.ts | 11 + .../codegen-ui/lib/utils/string-formatter.ts | 81 +- packages/codegen-ui/lib/validation-helper.ts | 48 +- packages/codegen-ui/package-lock.json | 473 ++- packages/codegen-ui/package.json | 1 + packages/test-generator/CHANGELOG.md | 15 +- .../cypress/e2e/complex-spec.cy.ts | 1 - .../cypress/e2e/form-spec.cy.ts | 82 + .../cypress/e2e/generate-spec.cy.ts | 2 + .../integration-test-templates/src/App.tsx | 5 + .../src/FormTests.tsx | 51 + .../variants/componentWithBreakpoint.json | 33 + .../lib/components/variants/index.ts | 1 + .../lib/forms/custom-form-create-dog.json | 48 + packages/test-generator/lib/forms/index.ts | 17 + .../lib/generators/BrowserTestGenerator.ts | 28 +- .../lib/generators/NodeTestGenerator.ts | 54 +- .../lib/generators/TestGenerator.ts | 77 +- packages/test-generator/lib/index.ts | 1 + packages/test-generator/lib/models/schema.ts | 364 ++ packages/test-generator/package-lock.json | 2 +- 159 files changed, 18430 insertions(+), 1444 deletions(-) create mode 100644 packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/form-state.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/type-helper.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/forms/type-helper.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/utils/__snapshots__/string-formatter.test.ts.snap create mode 100644 packages/codegen-ui-react/lib/__tests__/utils/fetch-by-path.test.ts create mode 100644 packages/codegen-ui-react/lib/__tests__/utils/string-formatter.test.ts create mode 100644 packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts create mode 100644 packages/codegen-ui-react/lib/forms/event-targets.ts create mode 100644 packages/codegen-ui-react/lib/forms/form-renderer-helper.ts create mode 100644 packages/codegen-ui-react/lib/forms/form-state.ts create mode 100644 packages/codegen-ui-react/lib/forms/index.ts create mode 100644 packages/codegen-ui-react/lib/forms/react-form-renderer.ts create mode 100644 packages/codegen-ui-react/lib/forms/type-helper.ts create mode 100644 packages/codegen-ui-react/lib/forms/typescript-type-map.ts create mode 100644 packages/codegen-ui-react/lib/helpers/index.ts create mode 100644 packages/codegen-ui-react/lib/react-table-renderer-helper.ts create mode 100644 packages/codegen-ui-react/lib/react-table-renderer.ts create mode 100644 packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts create mode 100644 packages/codegen-ui-react/lib/utils/forms/array-field-component.ts create mode 100644 packages/codegen-ui-react/lib/utils/forms/layout-helpers.ts create mode 100644 packages/codegen-ui-react/lib/utils/generate-react-hooks.ts create mode 100644 packages/codegen-ui-react/lib/utils/json-path-fetch.ts create mode 100644 packages/codegen-ui-react/lib/utils/string-formatter.ts create mode 100644 packages/codegen-ui-react/lib/views/react-view-renderer.ts create mode 100644 packages/codegen-ui/example-schemas/componentWithBreakpoint.json create mode 100644 packages/codegen-ui/example-schemas/datastore/input-gallery.json create mode 100644 packages/codegen-ui/example-schemas/datastore/person.json create mode 100644 packages/codegen-ui/example-schemas/datastore/post-ds.json create mode 100644 packages/codegen-ui/example-schemas/datastore/post.json create mode 100644 packages/codegen-ui/example-schemas/forms/bio-nested-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/bio-nested-update.json create mode 100644 packages/codegen-ui/example-schemas/forms/custom-with-sectional-elements.json create mode 100644 packages/codegen-ui/example-schemas/forms/input-gallery-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/input-gallery-update.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-custom-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-custom-update.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-datastore-create-row.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-datastore-create.json create mode 100644 packages/codegen-ui/example-schemas/forms/post-datastore-update.json create mode 100644 packages/codegen-ui/example-schemas/views/post-table-custom-format.json create mode 100644 packages/codegen-ui/example-schemas/views/post-table-datastore.json create mode 100644 packages/codegen-ui/example-schemas/views/table-from-custom-json.json create mode 100644 packages/codegen-ui/example-schemas/views/table-from-datastore-no-header.json create mode 100644 packages/codegen-ui/example-schemas/views/table-from-datastore.json create mode 100644 packages/codegen-ui/lib/__tests__/__utils__/basic-form-definition.ts delete mode 100644 packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts delete mode 100644 packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts create mode 100644 packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts create mode 100644 packages/codegen-ui/lib/__tests__/generate-view-definition/generate-table-definition.test.ts create mode 100644 packages/codegen-ui/lib/__tests__/generate-view-definition/order-filter-columns.test.ts create mode 100644 packages/codegen-ui/lib/__tests__/utils/form-component-metadata.test.ts create mode 100644 packages/codegen-ui/lib/__tests__/utils/form-to-component/map-form-definition-to-component.test.ts delete mode 100644 packages/codegen-ui/lib/generate-form-definition/form-to-component.ts delete mode 100644 packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts create mode 100644 packages/codegen-ui/lib/generate-form-definition/helpers/map-cta.ts create mode 100644 packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts create mode 100644 packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts create mode 100644 packages/codegen-ui/lib/generate-view-definition/helpers/index.ts create mode 100644 packages/codegen-ui/lib/generate-view-definition/helpers/order-filter-columns.ts create mode 100644 packages/codegen-ui/lib/generate-view-definition/index.ts create mode 100644 packages/codegen-ui/lib/types/form/form-cta.ts create mode 100644 packages/codegen-ui/lib/types/form/form-metadata.ts create mode 100644 packages/codegen-ui/lib/types/view/defaults.ts create mode 100644 packages/codegen-ui/lib/types/view/table-definition.ts create mode 100644 packages/codegen-ui/lib/types/view/view-metadata.ts create mode 100644 packages/codegen-ui/lib/utils/breakpoint-utils.ts create mode 100644 packages/codegen-ui/lib/utils/form-component-metadata.ts create mode 100644 packages/codegen-ui/lib/utils/form-to-component/helpers/map-cta-buttons.ts create mode 100644 packages/codegen-ui/lib/utils/form-to-component/helpers/map-element-children.ts create mode 100644 packages/codegen-ui/lib/utils/form-to-component/index.ts create mode 100644 packages/codegen-ui/lib/utils/form-to-component/map-form-definition-to-component.ts create mode 100644 packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts create mode 100644 packages/test-generator/integration-test-templates/src/FormTests.tsx create mode 100644 packages/test-generator/lib/components/variants/componentWithBreakpoint.json create mode 100644 packages/test-generator/lib/forms/custom-form-create-dog.json create mode 100644 packages/test-generator/lib/forms/index.ts create mode 100644 packages/test-generator/lib/models/schema.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 2ab02871b..92f4aedf5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,7 +65,7 @@ jobs: - restore_node_modules - run: name: Unit Test - command: npm run test + command: npm run test:ci - run: name: Codecov command: npx codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c6c6efe..12278ae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,41 +5,25 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## [2.3.2](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.1...v2.3.2) (2022-07-22) - ### Bug Fixes -* limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) - - - - +- limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) ## [2.3.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.0...v2.3.1) (2022-07-15) - ### Bug Fixes -* change recommended ui-react version to ^3.1.0 ([#521](https://github.com/aws-amplify/amplify-codegen-ui/issues/521)) ([33b6d06](https://github.com/aws-amplify/amplify-codegen-ui/commit/33b6d0658b87acd1e6eadc8ae3cb8629b2258b4b)) - - - - +- change recommended ui-react version to ^3.1.0 ([#521](https://github.com/aws-amplify/amplify-codegen-ui/issues/521)) ([33b6d06](https://github.com/aws-amplify/amplify-codegen-ui/commit/33b6d0658b87acd1e6eadc8ae3cb8629b2258b4b)) # [2.3.0](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.1...v2.3.0) (2022-07-14) - ### Bug Fixes -* handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) - +- handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) ### Features -* adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) - - - - +- adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) ## [2.2.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.0...v2.2.1) (2022-06-15) diff --git a/commitlint.config.js b/commitlint.config.js index 12778a888..acb2b8ddf 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -6,5 +6,6 @@ module.exports = { extends: ['@commitlint/config-conventional', '@commitlint/config-lerna-scopes'], rules: { 'scope-enum': async (context) => [2, 'always', [...(await getPackages(context)), 'release']], + 'header-max-length': [2, 'always', 200], }, }; diff --git a/jest.config.js b/jest.config.js index 1112e4d7c..ff360cd83 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,5 +8,5 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'core', 'node'], collectCoverage: true, collectCoverageFrom: ['src/**/.(ts|tsx|js|jsx)$', '!src/**/*.test.(ts|tsx|js|jsx)$', '!src/**/*.d.ts'], - projects: ['/packages/codegen-ui-react'], + projects: ['/packages/codegen-ui', '/packages/codegen-ui-react'], }; diff --git a/packages/codegen-ui-react/CHANGELOG.md b/packages/codegen-ui-react/CHANGELOG.md index 25044cb91..d64ba5847 100644 --- a/packages/codegen-ui-react/CHANGELOG.md +++ b/packages/codegen-ui-react/CHANGELOG.md @@ -5,41 +5,25 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## [2.3.2](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.1...v2.3.2) (2022-07-22) - ### Bug Fixes -* limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) - - - - +- limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) ## [2.3.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.0...v2.3.1) (2022-07-15) - ### Bug Fixes -* change recommended ui-react version to ^3.1.0 ([#521](https://github.com/aws-amplify/amplify-codegen-ui/issues/521)) ([33b6d06](https://github.com/aws-amplify/amplify-codegen-ui/commit/33b6d0658b87acd1e6eadc8ae3cb8629b2258b4b)) - - - - +- change recommended ui-react version to ^3.1.0 ([#521](https://github.com/aws-amplify/amplify-codegen-ui/issues/521)) ([33b6d06](https://github.com/aws-amplify/amplify-codegen-ui/commit/33b6d0658b87acd1e6eadc8ae3cb8629b2258b4b)) # [2.3.0](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.1...v2.3.0) (2022-07-14) - ### Bug Fixes -* handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) - +- handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) ### Features -* adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) - - - - +- adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) ## [2.2.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.0...v2.2.1) (2022-06-15) diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/react-studio-template-renderer-helper.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/react-studio-template-renderer-helper.test.ts.snap index 0bdf11a75..801e8b534 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/react-studio-template-renderer-helper.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/react-studio-template-renderer-helper.test.ts.snap @@ -13,7 +13,7 @@ exports[`react-studio-template-renderer-helper transpile fails to transpile with exports[`react-studio-template-renderer-helper transpile fails to transpile with ScriptTarget Latest 1`] = `"ScriptTarget 99 not supported with type declarations enabled, expected one of [0,1,2,3,4,5,6,7,8]"`; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES3 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -24,7 +24,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES5 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -35,7 +35,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2015 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -46,7 +46,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2016 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -57,7 +57,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2017 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -68,7 +68,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2018 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -79,7 +79,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2019 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -90,7 +90,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2020 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -101,7 +101,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr `; exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2021 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap new file mode 100644 index 000000000..5d84af4e9 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap @@ -0,0 +1,3612 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`amplify form renderer tests custom form tests should render a custom backed form 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Flex, + Grid, + Radio, + RadioGroupField, + SelectField, + StepperField, + TextField, +} from \\"@aws-amplify/ui-react\\"; +export default function CustomDataForm(props) { + const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = + props; + const [name, setName] = React.useState(undefined); + const [email, setEmail] = React.useState(undefined); + const [city, setCity] = React.useState(undefined); + const [category, setCategory] = React.useState(undefined); + const [pages, setPages] = React.useState(0); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(undefined); + setEmail(undefined); + setCity(undefined); + setCategory(undefined); + setPages(0); + setErrors({}); + }; + const validations = { + name: [{ type: \\"Required\\" }], + email: [{ type: \\"Required\\" }], + city: [], + category: [], + pages: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + const modelFields = { + name, + email, + city, + category, + pages, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + await onSubmit(modelFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"CustomDataForm\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + email, + city, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + await runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email: value, + city, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.email ?? value; + } + if (errors.email?.hasError) { + await runValidationTasks(\\"email\\", value); + } + setEmail(value); + }} + onBlur={() => runValidationTasks(\\"email\\", email)} + errorMessage={errors.email?.errorMessage} + hasError={errors.email?.hasError} + {...getOverrideProps(overrides, \\"email\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email, + city: value, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.city ?? value; + } + if (errors.city?.hasError) { + await runValidationTasks(\\"city\\", value); + } + setCity(value); + }} + onBlur={() => runValidationTasks(\\"city\\", city)} + errorMessage={errors.city?.errorMessage} + hasError={errors.city?.hasError} + {...getOverrideProps(overrides, \\"city\\")} + > + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email, + city, + category: value, + pages, + }; + const result = onChange(modelFields); + value = result?.category ?? value; + } + if (errors.category?.hasError) { + await runValidationTasks(\\"category\\", value); + } + setCategory(value); + }} + onBlur={() => runValidationTasks(\\"category\\", category)} + errorMessage={errors.category?.errorMessage} + hasError={errors.category?.hasError} + {...getOverrideProps(overrides, \\"category\\")} + > + + + + + { + let value = e; + if (onChange) { + const modelFields = { + name, + email, + city, + category, + pages: value, + }; + const result = onChange(modelFields); + value = result?.pages ?? value; + } + if (errors.pages?.hasError) { + await runValidationTasks(\\"pages\\", value); + } + setPages(value); + }} + onBlur={() => runValidationTasks(\\"pages\\", pages)} + errorMessage={errors.pages?.errorMessage} + hasError={errors.pages?.hasError} + {...getOverrideProps(overrides, \\"pages\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render a custom backed form 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, RadioGroupFieldProps, SelectFieldProps, StepperFieldProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type CustomDataFormInputValues = { + name?: UseBaseOrValidationType; + email?: UseBaseOrValidationType; + city?: UseBaseOrValidationType; + category?: UseBaseOrValidationType; + pages?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type CustomDataFormOverridesProps = { + CustomDataFormGrid?: FormProps; + name?: FormProps; + email?: FormProps; + city?: FormProps; + category?: FormProps; + pages?: FormProps; +} & EscapeHatchProps; +export declare type CustomDataFormProps = React.PropsWithChildren<{ + overrides?: CustomDataFormOverridesProps | undefined | null; +} & { + onSubmit: (fields: CustomDataFormInputValues) => void; + onCancel?: () => void; + onChange?: (fields: CustomDataFormInputValues) => CustomDataFormInputValues; + onValidate?: CustomDataFormInputValues; +}>; +export default function CustomDataForm(props: CustomDataFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests custom form tests should render a custom backed form 3`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Flex, + Grid, + Radio, + RadioGroupField, + SelectField, + StepperField, + TextField, +} from \\"@aws-amplify/ui-react\\"; +export default function CustomDataForm(props) { + const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = + props; + const [name, setName] = React.useState(undefined); + const [email, setEmail] = React.useState(undefined); + const [city, setCity] = React.useState(undefined); + const [category, setCategory] = React.useState(undefined); + const [pages, setPages] = React.useState(0); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(undefined); + setEmail(undefined); + setCity(undefined); + setCategory(undefined); + setPages(0); + setErrors({}); + }; + React.useEffect(() => { + if (initialData) { + setName(initialData.name); + setEmail(initialData.email); + setCity(initialData.city); + setCategory(initialData.category); + setPages(initialData.pages); + } + }, []); + const validations = { + name: [{ type: \\"Required\\" }], + email: [{ type: \\"Required\\" }], + city: [], + category: [], + pages: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + const modelFields = { + name, + email, + city, + category, + pages, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + await onSubmit(modelFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"CustomDataForm\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + email, + city, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + await runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email: value, + city, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.email ?? value; + } + if (errors.email?.hasError) { + await runValidationTasks(\\"email\\", value); + } + setEmail(value); + }} + onBlur={() => runValidationTasks(\\"email\\", email)} + errorMessage={errors.email?.errorMessage} + hasError={errors.email?.hasError} + {...getOverrideProps(overrides, \\"email\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email, + city: value, + category, + pages, + }; + const result = onChange(modelFields); + value = result?.city ?? value; + } + if (errors.city?.hasError) { + await runValidationTasks(\\"city\\", value); + } + setCity(value); + }} + onBlur={() => runValidationTasks(\\"city\\", city)} + errorMessage={errors.city?.errorMessage} + hasError={errors.city?.hasError} + {...getOverrideProps(overrides, \\"city\\")} + > + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name, + email, + city, + category: value, + pages, + }; + const result = onChange(modelFields); + value = result?.category ?? value; + } + if (errors.category?.hasError) { + await runValidationTasks(\\"category\\", value); + } + setCategory(value); + }} + onBlur={() => runValidationTasks(\\"category\\", category)} + errorMessage={errors.category?.errorMessage} + hasError={errors.category?.hasError} + {...getOverrideProps(overrides, \\"category\\")} + > + + + + + { + let value = e; + if (onChange) { + const modelFields = { + name, + email, + city, + category, + pages: value, + }; + const result = onChange(modelFields); + value = result?.pages ?? value; + } + if (errors.pages?.hasError) { + await runValidationTasks(\\"pages\\", value); + } + setPages(value); + }} + onBlur={() => runValidationTasks(\\"pages\\", pages)} + errorMessage={errors.pages?.errorMessage} + hasError={errors.pages?.hasError} + {...getOverrideProps(overrides, \\"pages\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render a custom backed form 4`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, RadioGroupFieldProps, SelectFieldProps, StepperFieldProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type CustomDataFormInputValues = { + name?: UseBaseOrValidationType; + email?: UseBaseOrValidationType; + city?: UseBaseOrValidationType; + category?: UseBaseOrValidationType; + pages?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type CustomDataFormOverridesProps = { + CustomDataFormGrid?: FormProps; + name?: FormProps; + email?: FormProps; + city?: FormProps; + category?: FormProps; + pages?: FormProps; +} & EscapeHatchProps; +export declare type CustomDataFormProps = React.PropsWithChildren<{ + overrides?: CustomDataFormOverridesProps | undefined | null; +} & { + initialData?: CustomDataFormInputValues; + onSubmit: (fields: CustomDataFormInputValues) => void; + onCancel?: () => void; + onChange?: (fields: CustomDataFormInputValues) => CustomDataFormInputValues; + onValidate?: CustomDataFormInputValues; +}>; +export default function CustomDataForm(props: CustomDataFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests custom form tests should render nested json fields 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Flex, + Grid, + Heading, + TextField, + useTheme, +} from \\"@aws-amplify/ui-react\\"; +export default function NestedJson(props) { + const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = + props; + const { tokens } = useTheme(); + const [firstName, setFirstName] = React.useState(undefined); + const [lastName, setLastName] = React.useState(undefined); + const [bio, setBio] = React.useState({}); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setFirstName(undefined); + setLastName(undefined); + setBio({}); + setErrors({}); + }; + const validations = { + firstName: [], + lastName: [], + \\"bio.favoriteQuote\\": [], + \\"bio.favoriteAnimal\\": [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + const modelFields = { + firstName, + lastName, + bio, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + await onSubmit(modelFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"NestedJson\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName: value, + lastName, + bio, + }; + const result = onChange(modelFields); + value = result?.firstName ?? value; + } + if (errors.firstName?.hasError) { + await runValidationTasks(\\"firstName\\", value); + } + setFirstName(value); + }} + onBlur={() => runValidationTasks(\\"firstName\\", firstName)} + errorMessage={errors.firstName?.errorMessage} + hasError={errors.firstName?.hasError} + {...getOverrideProps(overrides, \\"firstName\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName: value, + bio, + }; + const result = onChange(modelFields); + value = result?.lastName ?? value; + } + if (errors.lastName?.hasError) { + await runValidationTasks(\\"lastName\\", value); + } + setLastName(value); + }} + onBlur={() => runValidationTasks(\\"lastName\\", lastName)} + errorMessage={errors.lastName?.errorMessage} + hasError={errors.lastName?.hasError} + {...getOverrideProps(overrides, \\"lastName\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName, + bio: { ...bio, favoriteQuote: value }, + }; + const result = onChange(modelFields); + value = result?.bio?.favoriteQuote ?? value; + } + if (errors.bio.favoriteQuote?.hasError) { + await runValidationTasks(\\"bio.favoriteQuote\\", value); + } + setBio({ ...bio, favoriteQuote: value }); + }} + onBlur={() => + runValidationTasks(\\"bio.favoriteQuote\\", bio.favoriteQuote) + } + errorMessage={errors[\\"bio.favoriteQuote\\"]?.errorMessage} + hasError={errors[\\"bio.favoriteQuote\\"]?.hasError} + {...getOverrideProps(overrides, \\"bio.favoriteQuote\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName, + bio: { ...bio, favoriteAnimal: value }, + }; + const result = onChange(modelFields); + value = result?.bio?.favoriteAnimal ?? value; + } + if (errors.bio.favoriteAnimal?.hasError) { + await runValidationTasks(\\"bio.favoriteAnimal\\", value); + } + setBio({ ...bio, favoriteAnimal: value }); + }} + onBlur={() => + runValidationTasks(\\"bio.favoriteAnimal\\", bio.favoriteAnimal) + } + errorMessage={errors[\\"bio.favoriteAnimal\\"]?.errorMessage} + hasError={errors[\\"bio.favoriteAnimal\\"]?.hasError} + {...getOverrideProps(overrides, \\"bio.favoriteAnimal\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render nested json fields 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, HeadingProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type NestedJsonInputValues = { + firstName?: UseBaseOrValidationType; + lastName?: UseBaseOrValidationType; + bio?: { + favoriteQuote?: UseBaseOrValidationType; + favoriteAnimal?: UseBaseOrValidationType; + }; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type NestedJsonOverridesProps = { + NestedJsonGrid?: FormProps; + firstName?: FormProps; + lastName?: FormProps; + bio?: FormProps; + \\"bio.favoriteQuote\\"?: FormProps; + \\"bio.favoriteAnimal\\"?: FormProps; +} & EscapeHatchProps; +export declare type NestedJsonProps = React.PropsWithChildren<{ + overrides?: NestedJsonOverridesProps | undefined | null; +} & { + onSubmit: (fields: NestedJsonInputValues) => void; + onCancel?: () => void; + onChange?: (fields: NestedJsonInputValues) => NestedJsonInputValues; + onValidate?: NestedJsonInputValues; +}>; +export default function NestedJson(props: NestedJsonProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests custom form tests should render nested json fields 3`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { Button, Flex, Grid, Heading, TextField } from \\"@aws-amplify/ui-react\\"; +export default function NestedJson(props) { + const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = + props; + const [firstName, setFirstName] = React.useState(undefined); + const [lastName, setLastName] = React.useState(undefined); + const [bio, setBio] = React.useState({}); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setFirstName(undefined); + setLastName(undefined); + setBio({}); + setErrors({}); + }; + React.useEffect(() => { + if (initialData) { + setFirstName(initialData.firstName); + setLastName(initialData.lastName); + setBio(initialData.bio); + } + }, []); + const validations = { + firstName: [], + lastName: [], + \\"bio.favoriteQuote\\": [], + \\"bio.favoriteAnimal\\": [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + const modelFields = { + firstName, + lastName, + bio, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + await onSubmit(modelFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"NestedJson\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName: value, + lastName, + bio, + }; + const result = onChange(modelFields); + value = result?.firstName ?? value; + } + if (errors.firstName?.hasError) { + await runValidationTasks(\\"firstName\\", value); + } + setFirstName(value); + }} + onBlur={() => runValidationTasks(\\"firstName\\", firstName)} + errorMessage={errors.firstName?.errorMessage} + hasError={errors.firstName?.hasError} + {...getOverrideProps(overrides, \\"firstName\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName: value, + bio, + }; + const result = onChange(modelFields); + value = result?.lastName ?? value; + } + if (errors.lastName?.hasError) { + await runValidationTasks(\\"lastName\\", value); + } + setLastName(value); + }} + onBlur={() => runValidationTasks(\\"lastName\\", lastName)} + errorMessage={errors.lastName?.errorMessage} + hasError={errors.lastName?.hasError} + {...getOverrideProps(overrides, \\"lastName\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName, + bio: { ...bio, favoriteQuote: value }, + }; + const result = onChange(modelFields); + value = result?.bio?.favoriteQuote ?? value; + } + if (errors.bio.favoriteQuote?.hasError) { + await runValidationTasks(\\"bio.favoriteQuote\\", value); + } + setBio({ ...bio, favoriteQuote: value }); + }} + onBlur={() => + runValidationTasks(\\"bio.favoriteQuote\\", bio.favoriteQuote) + } + errorMessage={errors[\\"bio.favoriteQuote\\"]?.errorMessage} + hasError={errors[\\"bio.favoriteQuote\\"]?.hasError} + {...getOverrideProps(overrides, \\"bio.favoriteQuote\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + firstName, + lastName, + bio: { ...bio, favoriteAnimal: value }, + }; + const result = onChange(modelFields); + value = result?.bio?.favoriteAnimal ?? value; + } + if (errors.bio.favoriteAnimal?.hasError) { + await runValidationTasks(\\"bio.favoriteAnimal\\", value); + } + setBio({ ...bio, favoriteAnimal: value }); + }} + onBlur={() => + runValidationTasks(\\"bio.favoriteAnimal\\", bio.favoriteAnimal) + } + errorMessage={errors[\\"bio.favoriteAnimal\\"]?.errorMessage} + hasError={errors[\\"bio.favoriteAnimal\\"]?.hasError} + {...getOverrideProps(overrides, \\"bio.favoriteAnimal\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render nested json fields 4`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, HeadingProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type NestedJsonInputValues = { + firstName?: UseBaseOrValidationType; + lastName?: UseBaseOrValidationType; + bio?: { + favoriteQuote?: UseBaseOrValidationType; + favoriteAnimal?: UseBaseOrValidationType; + }; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type NestedJsonOverridesProps = { + NestedJsonGrid?: FormProps; + firstName?: FormProps; + lastName?: FormProps; + bio?: FormProps; + \\"bio.favoriteQuote\\"?: FormProps; + \\"bio.favoriteAnimal\\"?: FormProps; +} & EscapeHatchProps; +export declare type NestedJsonProps = React.PropsWithChildren<{ + overrides?: NestedJsonOverridesProps | undefined | null; +} & { + initialData?: NestedJsonInputValues; + onSubmit: (fields: NestedJsonInputValues) => void; + onCancel?: () => void; + onChange?: (fields: NestedJsonInputValues) => NestedJsonInputValues; + onValidate?: NestedJsonInputValues; +}>; +export default function NestedJson(props: NestedJsonProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests custom form tests should render sectional elements 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Divider, + Flex, + Grid, + Heading, + Text, + TextField, +} from \\"@aws-amplify/ui-react\\"; +export default function CustomWithSectionalElements(props) { + const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = + props; + const [name, setName] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setName(undefined); + setErrors({}); + }; + const validations = { + name: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + const modelFields = { + name, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + await onSubmit(modelFields); + }} + {...rest} + {...getOverrideProps(overrides, \\"CustomWithSectionalElements\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + name: value, + }; + const result = onChange(modelFields); + value = result?.name ?? value; + } + if (errors.name?.hasError) { + await runValidationTasks(\\"name\\", value); + } + setName(value); + }} + onBlur={() => runValidationTasks(\\"name\\", name)} + errorMessage={errors.name?.errorMessage} + hasError={errors.name?.hasError} + {...getOverrideProps(overrides, \\"name\\")} + > + + + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests custom form tests should render sectional elements 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { DividerProps, GridProps, HeadingProps, TextFieldProps, TextProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type CustomWithSectionalElementsInputValues = { + name?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type CustomWithSectionalElementsOverridesProps = { + CustomWithSectionalElementsGrid?: FormProps; + myHeading?: FormProps; + name?: FormProps; + myText?: FormProps; + myDivider?: FormProps; +} & EscapeHatchProps; +export declare type CustomWithSectionalElementsProps = React.PropsWithChildren<{ + overrides?: CustomWithSectionalElementsOverridesProps | undefined | null; +} & { + onSubmit: (fields: CustomWithSectionalElementsInputValues) => void; + onCancel?: () => void; + onChange?: (fields: CustomWithSectionalElementsInputValues) => CustomWithSectionalElementsInputValues; + onValidate?: CustomWithSectionalElementsInputValues; +}>; +export default function CustomWithSectionalElements(props: CustomWithSectionalElementsProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a create form 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { Post } from \\"../models\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { Button, Flex, Grid, TextField } from \\"@aws-amplify/ui-react\\"; +import { DataStore } from \\"aws-amplify\\"; +export default function MyPostForm(props) { + const { + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const [caption, setCaption] = React.useState(undefined); + const [username, setUsername] = React.useState(undefined); + const [post_url, setPost_url] = React.useState(undefined); + const [profile_url, setProfile_url] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setCaption(undefined); + setUsername(undefined); + setPost_url(undefined); + setProfile_url(undefined); + setErrors({}); + }; + const validations = { + caption: [], + username: [], + post_url: [{ type: \\"URL\\" }], + profile_url: [{ type: \\"URL\\" }], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + caption, + username, + post_url, + profile_url, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + await DataStore.save(new Post(modelFields)); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"MyPostForm\\")} + > + + + + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + caption: value, + username, + post_url, + profile_url, + }; + const result = onChange(modelFields); + value = result?.caption ?? value; + } + if (errors.caption?.hasError) { + await runValidationTasks(\\"caption\\", value); + } + setCaption(value); + }} + onBlur={() => runValidationTasks(\\"caption\\", caption)} + errorMessage={errors.caption?.errorMessage} + hasError={errors.caption?.hasError} + {...getOverrideProps(overrides, \\"caption\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + caption, + username: value, + post_url, + profile_url, + }; + const result = onChange(modelFields); + value = result?.username ?? value; + } + if (errors.username?.hasError) { + await runValidationTasks(\\"username\\", value); + } + setUsername(value); + }} + onBlur={() => runValidationTasks(\\"username\\", username)} + errorMessage={errors.username?.errorMessage} + hasError={errors.username?.hasError} + {...getOverrideProps(overrides, \\"username\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + caption, + username, + post_url: value, + profile_url, + }; + const result = onChange(modelFields); + value = result?.post_url ?? value; + } + if (errors.post_url?.hasError) { + await runValidationTasks(\\"post_url\\", value); + } + setPost_url(value); + }} + onBlur={() => runValidationTasks(\\"post_url\\", post_url)} + errorMessage={errors.post_url?.errorMessage} + hasError={errors.post_url?.hasError} + {...getOverrideProps(overrides, \\"post_url\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + caption, + username, + post_url, + profile_url: value, + }; + const result = onChange(modelFields); + value = result?.profile_url ?? value; + } + if (errors.profile_url?.hasError) { + await runValidationTasks(\\"profile_url\\", value); + } + setProfile_url(value); + }} + onBlur={() => runValidationTasks(\\"profile_url\\", profile_url)} + errorMessage={errors.profile_url?.errorMessage} + hasError={errors.profile_url?.hasError} + {...getOverrideProps(overrides, \\"profile_url\\")} + > + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a create form 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type MyPostFormInputValues = { + caption?: UseBaseOrValidationType; + username?: UseBaseOrValidationType; + post_url?: UseBaseOrValidationType; + profile_url?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type MyPostFormOverridesProps = { + MyPostFormGrid?: FormProps; + caption?: FormProps; + username?: FormProps; + post_url?: FormProps; + profile_url?: FormProps; +} & EscapeHatchProps; +export declare type MyPostFormProps = React.PropsWithChildren<{ + overrides?: MyPostFormOverridesProps | undefined | null; +} & { + onSubmit?: (fields: MyPostFormInputValues) => MyPostFormInputValues; + onSuccess?: (fields: MyPostFormInputValues) => void; + onError?: (fields: MyPostFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: MyPostFormInputValues) => MyPostFormInputValues; + onValidate?: MyPostFormInputValues; +}>; +export default function MyPostForm(props: MyPostFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a update form 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { Post } from \\"../models\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Flex, + Grid, + TextAreaField, + TextField, +} from \\"@aws-amplify/ui-react\\"; +import { DataStore } from \\"aws-amplify\\"; +export default function MyPostForm(props) { + const { + id, + post, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const [TextAreaFieldbbd63464, setTextAreaFieldbbd63464] = + React.useState(undefined); + const [caption, setCaption] = React.useState(undefined); + const [username, setUsername] = React.useState(undefined); + const [profile_url, setProfile_url] = React.useState(undefined); + const [post_url, setPost_url] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setTextAreaFieldbbd63464(undefined); + setCaption(undefined); + setUsername(undefined); + setProfile_url(undefined); + setPost_url(undefined); + setErrors({}); + }; + const [postRecord, setPostRecord] = React.useState(post); + React.useEffect(() => { + const queryData = async () => { + const record = id ? await DataStore.query(Post, id) : post; + if (record) { + setPostRecord(record); + setTextAreaFieldbbd63464(record.TextAreaFieldbbd63464); + setCaption(record.caption); + setUsername(record.username); + setProfile_url(record.profile_url); + setPost_url(record.post_url); + } + }; + queryData(); + }, [id, post]); + const validations = { + TextAreaFieldbbd63464: [], + caption: [], + username: [], + profile_url: [{ type: \\"URL\\" }], + post_url: [{ type: \\"URL\\" }], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + TextAreaFieldbbd63464, + caption, + username, + profile_url, + post_url, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + await DataStore.save( + Post.copyOf(postRecord, (updated) => { + Object.assign(updated, modelFields); + }) + ); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"MyPostForm\\")} + > + + + + + + + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + TextAreaFieldbbd63464: value, + caption, + username, + profile_url, + post_url, + }; + const result = onChange(modelFields); + value = result?.TextAreaFieldbbd63464 ?? value; + } + if (errors.TextAreaFieldbbd63464?.hasError) { + await runValidationTasks(\\"TextAreaFieldbbd63464\\", value); + } + setTextAreaFieldbbd63464(value); + }} + onBlur={() => + runValidationTasks(\\"TextAreaFieldbbd63464\\", TextAreaFieldbbd63464) + } + errorMessage={errors.TextAreaFieldbbd63464?.errorMessage} + hasError={errors.TextAreaFieldbbd63464?.hasError} + {...getOverrideProps(overrides, \\"TextAreaFieldbbd63464\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + TextAreaFieldbbd63464, + caption: value, + username, + profile_url, + post_url, + }; + const result = onChange(modelFields); + value = result?.caption ?? value; + } + if (errors.caption?.hasError) { + await runValidationTasks(\\"caption\\", value); + } + setCaption(value); + }} + onBlur={() => runValidationTasks(\\"caption\\", caption)} + errorMessage={errors.caption?.errorMessage} + hasError={errors.caption?.hasError} + {...getOverrideProps(overrides, \\"caption\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + TextAreaFieldbbd63464, + caption, + username: value, + profile_url, + post_url, + }; + const result = onChange(modelFields); + value = result?.username ?? value; + } + if (errors.username?.hasError) { + await runValidationTasks(\\"username\\", value); + } + setUsername(value); + }} + onBlur={() => runValidationTasks(\\"username\\", username)} + errorMessage={errors.username?.errorMessage} + hasError={errors.username?.hasError} + {...getOverrideProps(overrides, \\"username\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + TextAreaFieldbbd63464, + caption, + username, + profile_url: value, + post_url, + }; + const result = onChange(modelFields); + value = result?.profile_url ?? value; + } + if (errors.profile_url?.hasError) { + await runValidationTasks(\\"profile_url\\", value); + } + setProfile_url(value); + }} + onBlur={() => runValidationTasks(\\"profile_url\\", profile_url)} + errorMessage={errors.profile_url?.errorMessage} + hasError={errors.profile_url?.hasError} + {...getOverrideProps(overrides, \\"profile_url\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + TextAreaFieldbbd63464, + caption, + username, + profile_url, + post_url: value, + }; + const result = onChange(modelFields); + value = result?.post_url ?? value; + } + if (errors.post_url?.hasError) { + await runValidationTasks(\\"post_url\\", value); + } + setPost_url(value); + }} + onBlur={() => runValidationTasks(\\"post_url\\", post_url)} + errorMessage={errors.post_url?.errorMessage} + hasError={errors.post_url?.hasError} + {...getOverrideProps(overrides, \\"post_url\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should generate a update form 2`] = ` +"import * as React from \\"react\\"; +import { Post } from \\"../models\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, TextAreaFieldProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type MyPostFormInputValues = { + TextAreaFieldbbd63464?: UseBaseOrValidationType; + caption?: UseBaseOrValidationType; + username?: UseBaseOrValidationType; + profile_url?: UseBaseOrValidationType; + post_url?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type MyPostFormOverridesProps = { + MyPostFormGrid?: FormProps; + TextAreaFieldbbd63464?: FormProps; + caption?: FormProps; + username?: FormProps; + profile_url?: FormProps; + post_url?: FormProps; +} & EscapeHatchProps; +export declare type MyPostFormProps = React.PropsWithChildren<{ + overrides?: MyPostFormOverridesProps | undefined | null; +} & { + id?: string; + post?: Post; + onSubmit?: (fields: MyPostFormInputValues) => MyPostFormInputValues; + onSuccess?: (fields: MyPostFormInputValues) => void; + onError?: (fields: MyPostFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: MyPostFormInputValues) => MyPostFormInputValues; + onValidate?: MyPostFormInputValues; +}>; +export default function MyPostForm(props: MyPostFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should render a form with multiple date types 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { InputGallery } from \\"../models\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Badge, + Button, + CheckboxField, + Divider, + Flex, + Grid, + Icon, + Radio, + RadioGroupField, + ScrollView, + TextField, + ToggleButton, +} from \\"@aws-amplify/ui-react\\"; +import { DataStore } from \\"aws-amplify\\"; +function ArrayField({ + items = [], + onChange, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, +}) { + const [selectedBadgeIndex, setSelectedBadgeIndex] = React.useState(); + const removeItem = async (removeIndex) => { + const newItems = items.filter((value, index) => index !== removeIndex); + await onChange(newItems); + setSelectedBadgeIndex(undefined); + }; + const addItem = async () => { + if (currentFieldValue.length && !hasError) { + const newItems = [...items]; + if (selectedBadgeIndex !== undefined) { + newItems[selectedBadgeIndex] = currentFieldValue; + setSelectedBadgeIndex(undefined); + } else { + newItems.push(currentFieldValue); + } + await onChange(newItems); + } + }; + return ( + + {children} + + + + + {!!items.length && ( + + {items.map((value, index) => { + return ( + { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + inputFieldRef?.current?.focus(); + }} + > + {value} + { + event.stopPropagation(); + removeItem(index); + }} + /> + + ); + })} + + )} + + + ); +} +export default function InputGalleryCreateForm(props) { + const { + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const [num, setNum] = React.useState(undefined); + const [rootbeer, setRootbeer] = React.useState(undefined); + const [attend, setAttend] = React.useState(undefined); + const [maybeSlide, setMaybeSlide] = React.useState(false); + const [maybeCheck, setMaybeCheck] = React.useState(undefined); + const [arrayTypeField, setArrayTypeField] = React.useState(undefined); + const [timestamp, setTimestamp] = React.useState(undefined); + const [ippy, setIppy] = React.useState(undefined); + const [timeisnow, setTimeisnow] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setNum(undefined); + setRootbeer(undefined); + setAttend(undefined); + setMaybeSlide(false); + setMaybeCheck(undefined); + setArrayTypeField(undefined); + setCurrentArrayTypeFieldValue(undefined); + setTimestamp(undefined); + setIppy(undefined); + setTimeisnow(undefined); + setErrors({}); + }; + const [currentArrayTypeFieldValue, setCurrentArrayTypeFieldValue] = + React.useState(\\"\\"); + const arrayTypeFieldRef = React.createRef(); + const validations = { + num: [], + rootbeer: [], + attend: [{ type: \\"Required\\" }], + maybeSlide: [], + maybeCheck: [], + arrayTypeField: [], + timestamp: [], + ippy: [{ type: \\"IpAddress\\" }], + timeisnow: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + await DataStore.save(new InputGallery(modelFields)); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"InputGalleryCreateForm\\")} + > + { + let value = parseInt(e.target.value); + if (onChange) { + const modelFields = { + num: value, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.num ?? value; + } + if (errors.num?.hasError) { + await runValidationTasks(\\"num\\", value); + } + setNum(value); + }} + onBlur={() => runValidationTasks(\\"num\\", num)} + errorMessage={errors.num?.errorMessage} + hasError={errors.num?.hasError} + {...getOverrideProps(overrides, \\"num\\")} + > + { + let value = Number(e.target.value); + if (onChange) { + const modelFields = { + num, + rootbeer: value, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.rootbeer ?? value; + } + if (errors.rootbeer?.hasError) { + await runValidationTasks(\\"rootbeer\\", value); + } + setRootbeer(value); + }} + onBlur={() => runValidationTasks(\\"rootbeer\\", rootbeer)} + errorMessage={errors.rootbeer?.errorMessage} + hasError={errors.rootbeer?.hasError} + {...getOverrideProps(overrides, \\"rootbeer\\")} + > + { + let value = e.target.value === \\"true\\"; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend: value, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.attend ?? value; + } + if (errors.attend?.hasError) { + await runValidationTasks(\\"attend\\", value); + } + setAttend(value); + }} + onBlur={() => runValidationTasks(\\"attend\\", attend)} + errorMessage={errors.attend?.errorMessage} + hasError={errors.attend?.hasError} + {...getOverrideProps(overrides, \\"attend\\")} + > + + + + { + let value = !maybeSlide; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide: value, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.maybeSlide ?? value; + } + if (errors.maybeSlide?.hasError) { + await runValidationTasks(\\"maybeSlide\\", value); + } + setMaybeSlide(value); + }} + onBlur={() => runValidationTasks(\\"maybeSlide\\", maybeSlide)} + errorMessage={errors.maybeSlide?.errorMessage} + hasError={errors.maybeSlide?.hasError} + {...getOverrideProps(overrides, \\"maybeSlide\\")} + > + { + let value = e.target.checked; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck: value, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.maybeCheck ?? value; + } + if (errors.maybeCheck?.hasError) { + await runValidationTasks(\\"maybeCheck\\", value); + } + setMaybeCheck(value); + }} + onBlur={() => runValidationTasks(\\"maybeCheck\\", maybeCheck)} + errorMessage={errors.maybeCheck?.errorMessage} + hasError={errors.maybeCheck?.hasError} + {...getOverrideProps(overrides, \\"maybeCheck\\")} + > + { + setArrayTypeField(items); + setCurrentArrayTypeFieldValue(\\"\\"); + }} + currentFieldValue={currentArrayTypeFieldValue} + items={arrayTypeField} + hasError={errors.arrayTypeField?.hasError} + setFieldValue={setCurrentArrayTypeFieldValue} + inputFieldRef={arrayTypeFieldRef} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField: value, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.arrayTypeField ?? value; + } + if (errors.arrayTypeField?.hasError) { + await runValidationTasks(\\"arrayTypeField\\", value); + } + setCurrentArrayTypeFieldValue(value); + }} + onBlur={() => + runValidationTasks(\\"arrayTypeField\\", currentArrayTypeFieldValue) + } + errorMessage={errors.arrayTypeField?.errorMessage} + hasError={errors.arrayTypeField?.hasError} + value={currentArrayTypeFieldValue} + ref={arrayTypeFieldRef} + {...getOverrideProps(overrides, \\"arrayTypeField\\")} + > + + { + let value = Number(new Date(e.target.value)); + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp: value, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.timestamp ?? value; + } + if (errors.timestamp?.hasError) { + await runValidationTasks(\\"timestamp\\", value); + } + setTimestamp(value); + }} + onBlur={() => runValidationTasks(\\"timestamp\\", timestamp)} + errorMessage={errors.timestamp?.errorMessage} + hasError={errors.timestamp?.hasError} + {...getOverrideProps(overrides, \\"timestamp\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy: value, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.ippy ?? value; + } + if (errors.ippy?.hasError) { + await runValidationTasks(\\"ippy\\", value); + } + setIppy(value); + }} + onBlur={() => runValidationTasks(\\"ippy\\", ippy)} + errorMessage={errors.ippy?.errorMessage} + hasError={errors.ippy?.hasError} + {...getOverrideProps(overrides, \\"ippy\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow: value, + }; + const result = onChange(modelFields); + value = result?.timeisnow ?? value; + } + if (errors.timeisnow?.hasError) { + await runValidationTasks(\\"timeisnow\\", value); + } + setTimeisnow(value); + }} + onBlur={() => runValidationTasks(\\"timeisnow\\", timeisnow)} + errorMessage={errors.timeisnow?.errorMessage} + hasError={errors.timeisnow?.hasError} + {...getOverrideProps(overrides, \\"timeisnow\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should render a form with multiple date types 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { CheckboxFieldProps, GridProps, RadioGroupFieldProps, TextFieldProps, ToggleButtonProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type InputGalleryCreateFormInputValues = { + num?: UseBaseOrValidationType; + rootbeer?: UseBaseOrValidationType; + attend?: UseBaseOrValidationType; + maybeSlide?: UseBaseOrValidationType; + maybeCheck?: UseBaseOrValidationType; + arrayTypeField?: UseBaseOrValidationType; + timestamp?: UseBaseOrValidationType; + ippy?: UseBaseOrValidationType; + timeisnow?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type InputGalleryCreateFormOverridesProps = { + InputGalleryCreateFormGrid?: FormProps; + num?: FormProps; + rootbeer?: FormProps; + attend?: FormProps; + maybeSlide?: FormProps; + maybeCheck?: FormProps; + arrayTypeField?: FormProps; + timestamp?: FormProps; + ippy?: FormProps; + timeisnow?: FormProps; +} & EscapeHatchProps; +export declare type InputGalleryCreateFormProps = React.PropsWithChildren<{ + overrides?: InputGalleryCreateFormOverridesProps | undefined | null; +} & { + onSubmit?: (fields: InputGalleryCreateFormInputValues) => InputGalleryCreateFormInputValues; + onSuccess?: (fields: InputGalleryCreateFormInputValues) => void; + onError?: (fields: InputGalleryCreateFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: InputGalleryCreateFormInputValues) => InputGalleryCreateFormInputValues; + onValidate?: InputGalleryCreateFormInputValues; +}>; +export default function InputGalleryCreateForm(props: InputGalleryCreateFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should render a form with multiple date types 3`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { InputGallery } from \\"../models\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Badge, + Button, + CheckboxField, + Divider, + Flex, + Grid, + Icon, + Radio, + RadioGroupField, + ScrollView, + TextField, + ToggleButton, +} from \\"@aws-amplify/ui-react\\"; +import { DataStore } from \\"aws-amplify\\"; +function ArrayField({ + items = [], + onChange, + inputFieldRef, + children, + hasError, + setFieldValue, + currentFieldValue, +}) { + const [selectedBadgeIndex, setSelectedBadgeIndex] = React.useState(); + const removeItem = async (removeIndex) => { + const newItems = items.filter((value, index) => index !== removeIndex); + await onChange(newItems); + setSelectedBadgeIndex(undefined); + }; + const addItem = async () => { + if (currentFieldValue.length && !hasError) { + const newItems = [...items]; + if (selectedBadgeIndex !== undefined) { + newItems[selectedBadgeIndex] = currentFieldValue; + setSelectedBadgeIndex(undefined); + } else { + newItems.push(currentFieldValue); + } + await onChange(newItems); + } + }; + return ( + + {children} + + + + + {!!items.length && ( + + {items.map((value, index) => { + return ( + { + setSelectedBadgeIndex(index); + setFieldValue(items[index]); + inputFieldRef?.current?.focus(); + }} + > + {value} + { + event.stopPropagation(); + removeItem(index); + }} + /> + + ); + })} + + )} + + + ); +} +export default function InputGalleryCreateForm(props) { + const { + id, + inputGallery, + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const [num, setNum] = React.useState(undefined); + const [rootbeer, setRootbeer] = React.useState(undefined); + const [attend, setAttend] = React.useState(undefined); + const [maybeSlide, setMaybeSlide] = React.useState(false); + const [maybeCheck, setMaybeCheck] = React.useState(undefined); + const [arrayTypeField, setArrayTypeField] = React.useState(undefined); + const [timestamp, setTimestamp] = React.useState(undefined); + const [ippy, setIppy] = React.useState(undefined); + const [timeisnow, setTimeisnow] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setNum(undefined); + setRootbeer(undefined); + setAttend(undefined); + setMaybeSlide(false); + setMaybeCheck(undefined); + setArrayTypeField(undefined); + setCurrentArrayTypeFieldValue(undefined); + setTimestamp(undefined); + setIppy(undefined); + setTimeisnow(undefined); + setErrors({}); + }; + const [inputGalleryRecord, setInputGalleryRecord] = + React.useState(inputGallery); + React.useEffect(() => { + const queryData = async () => { + const record = id + ? await DataStore.query(InputGallery, id) + : inputGallery; + if (record) { + setInputGalleryRecord(record); + setNum(record.num); + setRootbeer(record.rootbeer); + setAttend(record.attend); + setMaybeSlide(record.maybeSlide); + setMaybeCheck(record.maybeCheck); + setArrayTypeField(record.arrayTypeField); + setTimestamp(record.timestamp); + setIppy(record.ippy); + setTimeisnow(record.timeisnow); + } + }; + queryData(); + }, [id, inputGallery]); + const [currentArrayTypeFieldValue, setCurrentArrayTypeFieldValue] = + React.useState(\\"\\"); + const arrayTypeFieldRef = React.createRef(); + const validations = { + num: [], + rootbeer: [], + attend: [{ type: \\"Required\\" }], + maybeSlide: [], + maybeCheck: [], + arrayTypeField: [], + timestamp: [], + ippy: [{ type: \\"IpAddress\\" }], + timeisnow: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + await DataStore.save( + InputGallery.copyOf(inputGalleryRecord, (updated) => { + Object.assign(updated, modelFields); + }) + ); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"InputGalleryCreateForm\\")} + > + { + let value = parseInt(e.target.value); + if (onChange) { + const modelFields = { + num: value, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.num ?? value; + } + if (errors.num?.hasError) { + await runValidationTasks(\\"num\\", value); + } + setNum(value); + }} + onBlur={() => runValidationTasks(\\"num\\", num)} + errorMessage={errors.num?.errorMessage} + hasError={errors.num?.hasError} + {...getOverrideProps(overrides, \\"num\\")} + > + { + let value = Number(e.target.value); + if (onChange) { + const modelFields = { + num, + rootbeer: value, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.rootbeer ?? value; + } + if (errors.rootbeer?.hasError) { + await runValidationTasks(\\"rootbeer\\", value); + } + setRootbeer(value); + }} + onBlur={() => runValidationTasks(\\"rootbeer\\", rootbeer)} + errorMessage={errors.rootbeer?.errorMessage} + hasError={errors.rootbeer?.hasError} + {...getOverrideProps(overrides, \\"rootbeer\\")} + > + { + let value = e.target.value === \\"true\\"; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend: value, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.attend ?? value; + } + if (errors.attend?.hasError) { + await runValidationTasks(\\"attend\\", value); + } + setAttend(value); + }} + onBlur={() => runValidationTasks(\\"attend\\", attend)} + errorMessage={errors.attend?.errorMessage} + hasError={errors.attend?.hasError} + {...getOverrideProps(overrides, \\"attend\\")} + > + + + + { + let value = !maybeSlide; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide: value, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.maybeSlide ?? value; + } + if (errors.maybeSlide?.hasError) { + await runValidationTasks(\\"maybeSlide\\", value); + } + setMaybeSlide(value); + }} + onBlur={() => runValidationTasks(\\"maybeSlide\\", maybeSlide)} + errorMessage={errors.maybeSlide?.errorMessage} + hasError={errors.maybeSlide?.hasError} + {...getOverrideProps(overrides, \\"maybeSlide\\")} + > + { + let value = e.target.checked; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck: value, + arrayTypeField, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.maybeCheck ?? value; + } + if (errors.maybeCheck?.hasError) { + await runValidationTasks(\\"maybeCheck\\", value); + } + setMaybeCheck(value); + }} + onBlur={() => runValidationTasks(\\"maybeCheck\\", maybeCheck)} + errorMessage={errors.maybeCheck?.errorMessage} + hasError={errors.maybeCheck?.hasError} + {...getOverrideProps(overrides, \\"maybeCheck\\")} + > + { + setArrayTypeField(items); + setCurrentArrayTypeFieldValue(\\"\\"); + }} + currentFieldValue={currentArrayTypeFieldValue} + items={arrayTypeField} + hasError={errors.arrayTypeField?.hasError} + setFieldValue={setCurrentArrayTypeFieldValue} + inputFieldRef={arrayTypeFieldRef} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField: value, + timestamp, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.arrayTypeField ?? value; + } + if (errors.arrayTypeField?.hasError) { + await runValidationTasks(\\"arrayTypeField\\", value); + } + setCurrentArrayTypeFieldValue(value); + }} + onBlur={() => + runValidationTasks(\\"arrayTypeField\\", currentArrayTypeFieldValue) + } + errorMessage={errors.arrayTypeField?.errorMessage} + hasError={errors.arrayTypeField?.hasError} + value={currentArrayTypeFieldValue} + ref={arrayTypeFieldRef} + {...getOverrideProps(overrides, \\"arrayTypeField\\")} + > + + { + let value = Number(new Date(e.target.value)); + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp: value, + ippy, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.timestamp ?? value; + } + if (errors.timestamp?.hasError) { + await runValidationTasks(\\"timestamp\\", value); + } + setTimestamp(value); + }} + onBlur={() => runValidationTasks(\\"timestamp\\", timestamp)} + errorMessage={errors.timestamp?.errorMessage} + hasError={errors.timestamp?.hasError} + {...getOverrideProps(overrides, \\"timestamp\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy: value, + timeisnow, + }; + const result = onChange(modelFields); + value = result?.ippy ?? value; + } + if (errors.ippy?.hasError) { + await runValidationTasks(\\"ippy\\", value); + } + setIppy(value); + }} + onBlur={() => runValidationTasks(\\"ippy\\", ippy)} + errorMessage={errors.ippy?.errorMessage} + hasError={errors.ippy?.hasError} + {...getOverrideProps(overrides, \\"ippy\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + num, + rootbeer, + attend, + maybeSlide, + maybeCheck, + arrayTypeField, + timestamp, + ippy, + timeisnow: value, + }; + const result = onChange(modelFields); + value = result?.timeisnow ?? value; + } + if (errors.timeisnow?.hasError) { + await runValidationTasks(\\"timeisnow\\", value); + } + setTimeisnow(value); + }} + onBlur={() => runValidationTasks(\\"timeisnow\\", timeisnow)} + errorMessage={errors.timeisnow?.errorMessage} + hasError={errors.timeisnow?.hasError} + {...getOverrideProps(overrides, \\"timeisnow\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should render a form with multiple date types 4`] = ` +"import * as React from \\"react\\"; +import { InputGallery } from \\"../models\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { CheckboxFieldProps, GridProps, RadioGroupFieldProps, TextFieldProps, ToggleButtonProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type InputGalleryCreateFormInputValues = { + num?: UseBaseOrValidationType; + rootbeer?: UseBaseOrValidationType; + attend?: UseBaseOrValidationType; + maybeSlide?: UseBaseOrValidationType; + maybeCheck?: UseBaseOrValidationType; + arrayTypeField?: UseBaseOrValidationType; + timestamp?: UseBaseOrValidationType; + ippy?: UseBaseOrValidationType; + timeisnow?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type InputGalleryCreateFormOverridesProps = { + InputGalleryCreateFormGrid?: FormProps; + num?: FormProps; + rootbeer?: FormProps; + attend?: FormProps; + maybeSlide?: FormProps; + maybeCheck?: FormProps; + arrayTypeField?: FormProps; + timestamp?: FormProps; + ippy?: FormProps; + timeisnow?: FormProps; +} & EscapeHatchProps; +export declare type InputGalleryCreateFormProps = React.PropsWithChildren<{ + overrides?: InputGalleryCreateFormOverridesProps | undefined | null; +} & { + id?: string; + inputGallery?: InputGallery; + onSubmit?: (fields: InputGalleryCreateFormInputValues) => InputGalleryCreateFormInputValues; + onSuccess?: (fields: InputGalleryCreateFormInputValues) => void; + onError?: (fields: InputGalleryCreateFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: InputGalleryCreateFormInputValues) => InputGalleryCreateFormInputValues; + onValidate?: InputGalleryCreateFormInputValues; +}>; +export default function InputGalleryCreateForm(props: InputGalleryCreateFormProps): React.ReactElement; +" +`; + +exports[`amplify form renderer tests datastore form tests should render form with a two inputs in row 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { fetchByPath, validateField } from \\"./utils\\"; +import { Post } from \\"../models\\"; +import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; +import { + Button, + Flex, + Grid, + SelectField, + TextField, +} from \\"@aws-amplify/ui-react\\"; +import { DataStore } from \\"aws-amplify\\"; +export default function PostCreateFormRow(props) { + const { + onSuccess, + onError, + onSubmit, + onCancel, + onValidate, + onChange, + overrides, + ...rest + } = props; + const [username, setUsername] = React.useState(undefined); + const [caption, setCaption] = React.useState(undefined); + const [post_url, setPost_url] = React.useState(undefined); + const [profile_url, setProfile_url] = React.useState(undefined); + const [status, setStatus] = React.useState(undefined); + const [errors, setErrors] = React.useState({}); + const resetStateValues = () => { + setUsername(undefined); + setCaption(undefined); + setPost_url(undefined); + setProfile_url(undefined); + setStatus(undefined); + setErrors({}); + }; + const validations = { + username: [ + { + type: \\"GreaterThanChar\\", + numValues: [2], + validationMessage: \\"needs to be of length 2\\", + }, + ], + caption: [], + post_url: [{ type: \\"URL\\" }], + profile_url: [{ type: \\"URL\\" }], + status: [], + }; + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + const customValidator = fetchByPath(onValidate, fieldName); + if (customValidator) { + validationResponse = await customValidator(value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + return ( + { + event.preventDefault(); + let modelFields = { + username, + caption, + post_url, + profile_url, + status, + }; + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push( + ...modelFields[fieldName].map((item) => + runValidationTasks(fieldName, item) + ) + ); + return promises; + } + promises.push( + runValidationTasks(fieldName, modelFields[fieldName]) + ); + return promises; + }, []) + ); + if (validationResponses.some((r) => r.hasError)) { + return; + } + if (onSubmit) { + modelFields = onSubmit(modelFields); + } + try { + await DataStore.save(new Post(modelFields)); + if (onSuccess) { + onSuccess(modelFields); + } + } catch (err) { + if (onError) { + onError(modelFields, err.message); + } + } + }} + {...rest} + {...getOverrideProps(overrides, \\"PostCreateFormRow\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + username: value, + caption, + post_url, + profile_url, + status, + }; + const result = onChange(modelFields); + value = result?.username ?? value; + } + if (errors.username?.hasError) { + await runValidationTasks(\\"username\\", value); + } + setUsername(value); + }} + onBlur={() => runValidationTasks(\\"username\\", username)} + errorMessage={errors.username?.errorMessage} + hasError={errors.username?.hasError} + {...getOverrideProps(overrides, \\"username\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + username, + caption: value, + post_url, + profile_url, + status, + }; + const result = onChange(modelFields); + value = result?.caption ?? value; + } + if (errors.caption?.hasError) { + await runValidationTasks(\\"caption\\", value); + } + setCaption(value); + }} + onBlur={() => runValidationTasks(\\"caption\\", caption)} + errorMessage={errors.caption?.errorMessage} + hasError={errors.caption?.hasError} + {...getOverrideProps(overrides, \\"caption\\")} + > + + { + let { value } = e.target; + if (onChange) { + const modelFields = { + username, + caption, + post_url: value, + profile_url, + status, + }; + const result = onChange(modelFields); + value = result?.post_url ?? value; + } + if (errors.post_url?.hasError) { + await runValidationTasks(\\"post_url\\", value); + } + setPost_url(value); + }} + onBlur={() => runValidationTasks(\\"post_url\\", post_url)} + errorMessage={errors.post_url?.errorMessage} + hasError={errors.post_url?.hasError} + {...getOverrideProps(overrides, \\"post_url\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + username, + caption, + post_url, + profile_url: value, + status, + }; + const result = onChange(modelFields); + value = result?.profile_url ?? value; + } + if (errors.profile_url?.hasError) { + await runValidationTasks(\\"profile_url\\", value); + } + setProfile_url(value); + }} + onBlur={() => runValidationTasks(\\"profile_url\\", profile_url)} + errorMessage={errors.profile_url?.errorMessage} + hasError={errors.profile_url?.hasError} + {...getOverrideProps(overrides, \\"profile_url\\")} + > + { + let { value } = e.target; + if (onChange) { + const modelFields = { + username, + caption, + post_url, + profile_url, + status: value, + }; + const result = onChange(modelFields); + value = result?.status ?? value; + } + if (errors.status?.hasError) { + await runValidationTasks(\\"status\\", value); + } + setStatus(value); + }} + onBlur={() => runValidationTasks(\\"status\\", status)} + errorMessage={errors.status?.errorMessage} + hasError={errors.status?.hasError} + {...getOverrideProps(overrides, \\"status\\")} + > + + + + + + + + + ); +} +" +`; + +exports[`amplify form renderer tests datastore form tests should render form with a two inputs in row 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +import { GridProps, SelectFieldProps, TextFieldProps } from \\"@aws-amplify/ui-react\\"; +export declare type ValidationResponse = { + hasError: boolean; + errorMessage?: string; +}; +export declare type ValidationFunction = (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; +export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; +export declare type PostCreateFormRowInputValues = { + username?: UseBaseOrValidationType; + caption?: UseBaseOrValidationType; + post_url?: UseBaseOrValidationType; + profile_url?: UseBaseOrValidationType; + status?: UseBaseOrValidationType; +}; +export declare type FormProps = Partial & React.DOMAttributes; +export declare type PostCreateFormRowOverridesProps = { + PostCreateFormRowGrid?: FormProps; + RowGrid0?: FormProps; + username?: FormProps; + caption?: FormProps; + post_url?: FormProps; + profile_url?: FormProps; + status?: FormProps; +} & EscapeHatchProps; +export declare type PostCreateFormRowProps = React.PropsWithChildren<{ + overrides?: PostCreateFormRowOverridesProps | undefined | null; +} & { + onSubmit?: (fields: PostCreateFormRowInputValues) => PostCreateFormRowInputValues; + onSuccess?: (fields: PostCreateFormRowInputValues) => void; + onError?: (fields: PostCreateFormRowInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: PostCreateFormRowInputValues) => PostCreateFormRowInputValues; + onValidate?: PostCreateFormRowInputValues; +}>; +export default function PostCreateFormRow(props: PostCreateFormRowProps): React.ReactElement; +" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap new file mode 100644 index 000000000..0cfe0c320 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap @@ -0,0 +1,378 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`amplify table renderer tests should generate a non-datastore table element 1`] = ` +" + {!disableHeaders && ( + + + name + age + address + birthday + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + + {format?.name ? format.name(item?.name) : item?.name} + + {format?.age ? format.age(item?.age) : item?.age} + + {format?.address ? format.address(item?.address) : item?.address} + + + {format?.birthday ? format.birthday(item?.birthday) : item?.birthday} + + + ))} + +
; +" +`; + +exports[`amplify table renderer tests should generate a table element 1`] = ` +" + {!disableHeaders && ( + + + hireDate + comments + createdAt + updatedAt + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + + {format?.hireDate + ? format.hireDate(item?.hireDate) + : formatter(item?.hireDate, { + type: \\"NonLocaleDateTimeFormat\\", + format: { + nonLocaleDateTimeFormat: { + dateFormat: \\"locale\\", + timeFormat: \\"hours24\\", + }, + }, + })} + + + {format?.comments ? format.comments(item?.comments) : item?.comments} + + + {format?.createdAt + ? format.createdAt(item?.createdAt) + : formatter(item?.createdAt, { + type: \\"TimeFormat\\", + format: { timeFormat: \\"hours24\\" }, + })} + + + {format?.updatedAt + ? format.updatedAt(item?.updatedAt) + : formatter(item?.updatedAt, { + type: \\"DateFormat\\", + format: { dateFormat: \\"Mmm, DD YYYY\\" }, + })} + + + ))} + +
; +" +`; + +exports[`amplify view renderer tests should call util file if rendered 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { formatter } from \\"./utils\\"; +import { + createDataStorePredicate, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { SortDirection } from \\"@aws-amplify/datastore\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function MyPostTable(props) { + const { + items: itemsProps, + predicateOverride, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + const postFilter = { + and: [ + { field: \\"username\\", operand: \\"Guy\\", operator: \\"notContains\\" }, + { field: \\"createdAt\\", operand: \\"25\\", operator: \\"contains\\" }, + ], + }; + const postPredicate = createDataStorePredicate(postFilter); + const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) }; + const MyPostTableDataStore = useDataStoreBinding({ + type: \\"collection\\", + model: Post, + criteria: predicateOverride || postPredicate, + pagination: postPagination, + }).items; + const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore; + return ( + + {!disableHeaders && ( + + + id + caption + username + post_url + profile_url + status + createdAt + updatedAt + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + {format?.id ? format.id(item?.id) : item?.id} + + {format?.caption ? format.caption(item?.caption) : item?.caption} + + + {format?.username + ? format.username(item?.username) + : item?.username} + + + {format?.post_url + ? format.post_url(item?.post_url) + : item?.post_url} + + + {format?.profile_url + ? format.profile_url(item?.profile_url) + : item?.profile_url} + + + {format?.status ? format.status(item?.status) : item?.status} + + + {format?.createdAt + ? format.createdAt(item?.createdAt) + : formatter(item?.createdAt, { + type: \\"LocaleDateTimeFormat\\", + format: { localeDateTimeFormat: \\"locale\\" }, + })} + + + {format?.updatedAt + ? format.updatedAt(item?.updatedAt) + : item?.updatedAt} + + + ))} + +
+ ); +} +" +`; + +exports[`amplify view renderer tests should call util file if rendered 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type MyPostTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +}>; +export default function MyPostTable(props: MyPostTableProps): React.ReactElement; +" +`; + +exports[`amplify view renderer tests should render view with custom datastore 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function CustomTable(props) { + const { + items, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + return ( + + {!disableHeaders && ( + + + name + age + address + birthday + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + + {format?.name ? format.name(item?.name) : item?.name} + + + {format?.age ? format.age(item?.age) : item?.age} + + + {format?.address ? format.address(item?.address) : item?.address} + + + {format?.birthday + ? format.birthday(item?.birthday) + : item?.birthday} + + + ))} + +
+ ); +} +" +`; + +exports[`amplify view renderer tests should render view with custom datastore 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type CustomTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +}>; +export default function CustomTable(props: CustomTableProps): React.ReactElement; +" +`; + +exports[`amplify view renderer tests should render view with passed in predicate and sort 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + createDataStorePredicate, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { SortDirection } from \\"@aws-amplify/datastore\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function MyPostTable(props) { + const { + items: itemsProps, + predicateOverride, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + const postFilter = { + and: [ + { field: \\"username\\", operand: \\"username0\\", operator: \\"notContains\\" }, + { field: \\"createdAt\\", operand: \\"2022\\", operator: \\"contains\\" }, + ], + }; + const postPredicate = createDataStorePredicate(postFilter); + const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) }; + const MyPostTableDataStore = useDataStoreBinding({ + type: \\"collection\\", + model: Post, + criteria: predicateOverride || postPredicate, + pagination: postPagination, + }).items; + const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore; + return ( + + {!disableHeaders && ( + + + id + caption + username + post_url + profile_url + status + createdAt + updatedAt + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + {format?.id ? format.id(item?.id) : item?.id} + + {format?.caption ? format.caption(item?.caption) : item?.caption} + + + {format?.username + ? format.username(item?.username) + : item?.username} + + + {format?.post_url + ? format.post_url(item?.post_url) + : item?.post_url} + + + {format?.profile_url + ? format.profile_url(item?.profile_url) + : item?.profile_url} + + + {format?.status ? format.status(item?.status) : item?.status} + + + {format?.createdAt + ? format.createdAt(item?.createdAt) + : item?.createdAt} + + + {format?.updatedAt + ? format.updatedAt(item?.updatedAt) + : item?.updatedAt} + + + ))} + +
+ ); +} +" +`; + +exports[`amplify view renderer tests should render view with passed in predicate and sort 2`] = ` +"import * as React from \\"react\\"; +import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; +export declare type MyPostTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +}>; +export default function MyPostTable(props: MyPostTableProps): React.ReactElement; +" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap index d6417f071..8b71daa80 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap @@ -3,7 +3,7 @@ exports[`amplify render tests actions DataStore DataStoreCreateItem 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -48,7 +48,7 @@ export default function CreateCustomerButton( exports[`amplify render tests actions DataStore DataStoreDeleteItem 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -93,7 +93,7 @@ export default function DeleteCustomerButton( exports[`amplify render tests actions DataStore DataStoreUpdateItem 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -139,7 +139,7 @@ export default function UpdateCustomerButton( exports[`amplify render tests actions auth signs out 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -178,7 +178,7 @@ export default function SignOutButton( exports[`amplify render tests actions navigation anchor navigation action 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -220,7 +220,7 @@ export default function NavigateButton( exports[`amplify render tests actions navigation hard navigation action 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -262,7 +262,7 @@ export default function NavigateButton( exports[`amplify render tests actions navigation new tab navigation action 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -305,7 +305,7 @@ export default function NavigateButton( exports[`amplify render tests actions navigation reload navigation action 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -344,7 +344,7 @@ export default function ReloadButton( exports[`amplify render tests actions with conditional in parameters 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -410,7 +410,7 @@ export default function ConditionalInMutation( exports[`amplify render tests basic component tests should generate a simple button component 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -442,7 +442,7 @@ export default function CustomButton( exports[`amplify render tests basic component tests should generate a simple text component 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -472,7 +472,7 @@ export default function CustomText(props: CustomTextProps): React.ReactElement { exports[`amplify render tests basic component tests should generate a simple view component 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -501,7 +501,7 @@ export default function Test(props: TestProps): React.ReactElement { exports[`amplify render tests bindings auth supports auth bindings in actions 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -547,7 +547,7 @@ export default function ComponentWithAuthEventBinding( exports[`amplify render tests bindings data supports bindings with reserved keywords 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { Class } from \\"../models\\"; import { EscapeHatchProps, @@ -580,7 +580,7 @@ export default function DataBindingNamedClass( exports[`amplify render tests collection should not render nested query if the data schema is not provided 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { Author } from \\"../models\\"; import { EscapeHatchProps, @@ -638,7 +638,7 @@ export default function AuthorProfileCollection( exports[`amplify render tests collection should render collection with data binding 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -741,7 +741,7 @@ export default function CollectionOfCustomButtons( exports[`amplify render tests collection should render collection with data binding and sort 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -852,7 +852,7 @@ export default function CollectionOfCustomButtons( exports[`amplify render tests collection should render collection with data binding if binding name is items 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -954,7 +954,7 @@ export default function CollectionOfCustomButtons( exports[`amplify render tests collection should render collection with data binding with no predicate 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { UntitledModel } from \\"../models\\"; import { EscapeHatchProps, @@ -1014,7 +1014,7 @@ export default function ListingCardCollection( exports[`amplify render tests collection should render collection without data binding 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import ListingCard, { ListingCardProps } from \\"./ListingCard\\"; import { EscapeHatchProps, @@ -1060,7 +1060,7 @@ export default function ListingCardCollection( exports[`amplify render tests collection should render nested query if model has a hasMany relationship 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { Author, Book } from \\"../models\\"; import { EscapeHatchProps, @@ -1125,7 +1125,7 @@ export default function AuthorProfileCollection( exports[`amplify render tests complex component tests should generate a button within a view component 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1157,7 +1157,7 @@ export default function ViewWithButton( exports[`amplify render tests complex component tests should generate a component with custom child 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1190,7 +1190,7 @@ export default function ViewWithCustomButton( exports[`amplify render tests complex component tests should generate a component with exposeAs prop 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1222,7 +1222,7 @@ export default function ViewWithButton( exports[`amplify render tests complex examples should render complex sample 1 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1273,7 +1273,7 @@ export default function ComplexTest1( exports[`amplify render tests complex examples should render complex sample 2 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1342,7 +1342,7 @@ export default function ComplexTest2( exports[`amplify render tests complex examples should render complex sample 3 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1423,7 +1423,7 @@ export default function ComplexTest3( exports[`amplify render tests complex examples should render complex sample 4 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -1431,12 +1431,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { - Flex, - FlexProps, - View, - useBreakpointValue, -} from \\"@aws-amplify/ui-react\\"; +import { Flex, FlexProps, View } from \\"@aws-amplify/ui-react\\"; export type ComplexTest4Props = React.PropsWithChildren< Partial & { @@ -1448,7 +1443,7 @@ export type ComplexTest4Props = React.PropsWithChildren< export default function ComplexTest4( props: ComplexTest4Props ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { overrides: { @@ -1491,20 +1486,8 @@ export default function ComplexTest4( variantValues: { colors: \\"Red/Orange\\" }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -1558,7 +1541,7 @@ export default function ComplexTest4( exports[`amplify render tests complex examples should render complex sample 5 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1612,7 +1595,7 @@ export default function ComplexTest5( exports[`amplify render tests complex examples should render complex sample 6 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1690,7 +1673,7 @@ export default function ComplexTest6( exports[`amplify render tests complex examples should render complex sample 7 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1764,7 +1747,7 @@ export default function ComplexTest7( exports[`amplify render tests complex examples should render complex sample 8 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -1831,7 +1814,7 @@ export default function ComplexTest8( exports[`amplify render tests complex examples should render complex sample 9 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -1839,13 +1822,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { - Button, - Flex, - FlexProps, - Text, - useBreakpointValue, -} from \\"@aws-amplify/ui-react\\"; +import { Button, Flex, FlexProps, Text } from \\"@aws-amplify/ui-react\\"; export type ComplexTest9Props = React.PropsWithChildren< Partial & { @@ -1857,7 +1834,7 @@ export type ComplexTest9Props = React.PropsWithChildren< export default function ComplexTest9( props: ComplexTest9Props ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { nodeId: \\"2878:3221\\", @@ -2091,20 +2068,8 @@ export default function ComplexTest9( }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -2240,7 +2205,7 @@ export default function ComplexTest9( exports[`amplify render tests complex examples should render complex sample 10 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -3127,7 +3092,7 @@ export default function ComplexTest10( exports[`amplify render tests complex examples should render complex sample 11 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4034,7 +3999,7 @@ export default function ComplexTest11( exports[`amplify render tests component with binding should render build property on Text 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4068,7 +4033,7 @@ export default function TextWithDataBinding( exports[`amplify render tests component with binding should render slot binding 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4104,7 +4069,7 @@ export default function ComponentWithSlotBinding( exports[`amplify render tests component with data binding should add model imports 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { User } from \\"../models\\"; import { EscapeHatchProps, @@ -4143,7 +4108,7 @@ export default function ComponentWithDataBinding( exports[`amplify render tests component with data binding should not have useDataStoreBinding when there is no predicate 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { UntitledModel } from \\"../models\\"; import { EscapeHatchProps, @@ -4204,7 +4169,7 @@ export default function SectionHeading( exports[`amplify render tests component with data binding should render with data binding in child elements 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4241,7 +4206,7 @@ export default function ChildComponentWithDataBinding( exports[`amplify render tests component with variants and not override children prop should render variants with options provided, and not override children prop 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -4249,7 +4214,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { View, ViewProps, useBreakpointValue } from \\"@aws-amplify/ui-react\\"; +import { View, ViewProps } from \\"@aws-amplify/ui-react\\"; export type ViewPrimitiveProps = React.PropsWithChildren< Partial & { @@ -4261,7 +4226,7 @@ export type ViewPrimitiveProps = React.PropsWithChildren< export default function ViewPrimitive( props: ViewPrimitiveProps ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { variantValues: { variant: \\"primary\\" }, @@ -4270,20 +4235,8 @@ export default function ViewPrimitive( }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -4304,7 +4257,7 @@ export default function ViewPrimitive( exports[`amplify render tests component with variants should render object variants 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -4312,7 +4265,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { Icon, IconProps, useBreakpointValue } from \\"@aws-amplify/ui-react\\"; +import { Icon, IconProps } from \\"@aws-amplify/ui-react\\"; export type IconVariantsProps = React.PropsWithChildren< Partial & { @@ -4324,7 +4277,7 @@ export type IconVariantsProps = React.PropsWithChildren< export default function IconVariants( props: IconVariantsProps ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { variantValues: { variant: \\"primary\\" }, @@ -4357,20 +4310,8 @@ export default function IconVariants( }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -4387,7 +4328,7 @@ export default function IconVariants( exports[`amplify render tests component with variants should render variants with options provided 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -4395,7 +4336,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps, useBreakpointValue } from \\"@aws-amplify/ui-react\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type CustomButtonProps = React.PropsWithChildren< Partial & { @@ -4408,7 +4349,7 @@ export type CustomButtonProps = React.PropsWithChildren< export default function CustomButton( props: CustomButtonProps ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { variantValues: { variant: \\"primary\\" }, @@ -4423,20 +4364,8 @@ export default function CustomButton( overrides: { CustomButton: { width: \\"500\\" } }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -4453,7 +4382,7 @@ export default function CustomButton( exports[`amplify render tests component with variants with mapped children prop should render variants with options provided, and mapped children prop 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, Variant, @@ -4461,7 +4390,7 @@ import { getOverridesFromVariants, mergeVariantsAndOverrides, } from \\"@aws-amplify/ui-react/internal\\"; -import { Button, ButtonProps, useBreakpointValue } from \\"@aws-amplify/ui-react\\"; +import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type CustomButtonProps = React.PropsWithChildren< Partial & { @@ -4474,7 +4403,7 @@ export type CustomButtonProps = React.PropsWithChildren< export default function CustomButton( props: CustomButtonProps ): React.ReactElement { - const { overrides: overridesProp, ...restProp } = props; + const { overrides: overridesProp, ...rest } = props; const variants: Variant[] = [ { variantValues: { variant: \\"primary\\" }, @@ -4500,20 +4429,8 @@ export default function CustomButton( }, }, ]; - const breakpointHook = useBreakpointValue({ - base: \\"base\\", - large: \\"large\\", - medium: \\"medium\\", - small: \\"small\\", - xl: \\"xl\\", - xxl: \\"xxl\\", - }); - const rest = { style: { transition: \\"all 0.25s\\" }, ...restProp }; const overrides = mergeVariantsAndOverrides( - getOverridesFromVariants(variants, { - breakpoint: breakpointHook, - ...props, - }), + getOverridesFromVariants(variants, props), overridesProp || {} ); return ( @@ -4529,7 +4446,7 @@ export default function CustomButton( exports[`amplify render tests concat and conditional transform should render child component with data bound concatenation 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4571,7 +4488,7 @@ export default function ChildComponentWithDataBoundConcatenation( exports[`amplify render tests concat and conditional transform should render child component with static concatenation 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4606,7 +4523,7 @@ export default function ChildComponentWithStaticConcatenation( exports[`amplify render tests concat and conditional transform should render component with concatenation prop 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { User } from \\"../models\\"; import { EscapeHatchProps, @@ -4644,7 +4561,7 @@ export default function CustomButton( exports[`amplify render tests concat and conditional transform should render component with conditional data binding prop 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { User } from \\"../models\\"; import { EscapeHatchProps, @@ -4695,7 +4612,7 @@ export default function CustomButton( exports[`amplify render tests concat and conditional transform should render component with conditional data binding prop from a bug 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { Student } from \\"../models\\"; import { EscapeHatchProps, @@ -4749,7 +4666,7 @@ export default function ConditionalComponentWithDataBinding( exports[`amplify render tests concat and conditional transform should render component with conditional simple binding prop 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4781,7 +4698,7 @@ export default function CustomButton( exports[`amplify render tests custom components custom children should render component with custom children 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4844,7 +4761,7 @@ var __rest = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; import CustomButton from \\"./CustomButton\\"; import MyView from \\"./MyView\\"; @@ -4864,7 +4781,7 @@ export default function CustomChildren(props) { `; exports[`amplify render tests custom components custom children should render declarations 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { MyViewProps } from \\"./MyView\\"; export declare type CustomChildrenProps = React.PropsWithChildren & { @@ -4876,7 +4793,7 @@ export default function CustomChildren(props: CustomChildrenProps): React.ReactE exports[`amplify render tests custom components custom parent and children should render component with custom parent and children 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -4942,7 +4859,7 @@ var __rest = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; import CustomButton from \\"./CustomButton\\"; import ViewTest from \\"./ViewTest\\"; @@ -4962,7 +4879,7 @@ export default function CustomParentAndChildren(props) { `; exports[`amplify render tests custom components custom parent and children should render declarations 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { ViewTestProps } from \\"./ViewTest\\"; export declare type CustomParentAndChildrenProps = React.PropsWithChildren & { @@ -4974,7 +4891,7 @@ export default function CustomParentAndChildren(props: CustomParentAndChildrenPr exports[`amplify render tests custom components custom parent should render component 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5035,7 +4952,7 @@ var __rest = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; import { Button } from \\"@aws-amplify/ui-react\\"; import MyView from \\"./MyView\\"; @@ -5055,7 +4972,7 @@ export default function CustomParent(props) { `; exports[`amplify render tests custom components custom parent should render declarations 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { MyViewProps } from \\"./MyView\\"; export declare type CustomParentProps = React.PropsWithChildren & { @@ -5099,7 +5016,7 @@ var __rest = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; import { Button, View } from \\"@aws-amplify/ui-react\\"; export default function ViewWithButton(props) { @@ -5139,7 +5056,7 @@ exports[`amplify render tests custom render config should render JSX 1`] = ` return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps } from \\"@aws-amplify/ui-react/internal\\"; import { Button, View } from \\"@aws-amplify/ui-react\\"; export default function ViewWithButton(props) { @@ -5160,6 +5077,43 @@ export default function ViewWithButton(props) { exports[`amplify render tests custom render config should render common JS 1`] = ` "\\"use strict\\"; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, \\"default\\", { enumerable: true, value: v }); + } + : function (o, v) { + o[\\"default\\"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== \\"default\\" && Object.prototype.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; var __rest = (this && this.__rest) || function (s, e) { @@ -5177,27 +5131,22 @@ var __rest = } return t; }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; Object.defineProperty(exports, \\"__esModule\\", { value: true }); /* eslint-disable */ -const react_1 = __importDefault(require(\\"react\\")); +const React = __importStar(require(\\"react\\")); const internal_1 = require(\\"@aws-amplify/ui-react/internal\\"); const ui_react_1 = require(\\"@aws-amplify/ui-react\\"); function ViewWithButton(props) { const { overrides } = props, rest = __rest(props, [\\"overrides\\"]); - return react_1.default.createElement( + return React.createElement( ui_react_1.View, Object.assign( {}, rest, (0, internal_1.getOverrideProps)(overrides, \\"ViewWithButton\\") ), - react_1.default.createElement( + React.createElement( ui_react_1.Button, Object.assign( { color: \\"#ff0000\\", width: \\"20px\\" }, @@ -5211,7 +5160,7 @@ exports.default = ViewWithButton; `; exports[`amplify render tests declarations should render declarations 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; import { FlexProps } from \\"@aws-amplify/ui-react\\"; export declare type ProfileProps = React.PropsWithChildren & { @@ -5224,7 +5173,7 @@ export default function Profile(props: ProfileProps): React.ReactElement; exports[`amplify render tests default value should render bound default value 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5260,7 +5209,7 @@ export default function BoundDefaultValue( exports[`amplify render tests default value should render collection default value 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { User } from \\"../models\\"; import { EscapeHatchProps, @@ -5316,7 +5265,7 @@ export default function CollectionDefaultValue( exports[`amplify render tests default value should render simple and bound default value 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5352,7 +5301,7 @@ export default function SimpleAndBoundDefaultValue( exports[`amplify render tests default value should render simple default value 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5387,7 +5336,7 @@ export default function SimplePropertyBindingDefaultValue( exports[`amplify render tests icon-indices does not return negative indices for icons 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5687,7 +5636,7 @@ export default function SocialA(props: SocialAProps): React.ReactElement { exports[`amplify render tests mutations controls an input that is modified by a button 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5737,7 +5686,7 @@ export default function InputMutationOnClick( exports[`amplify render tests mutations form 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5793,7 +5742,7 @@ export default function MyForm(props: MyFormProps): React.ReactElement { exports[`amplify render tests mutations internal mutation 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5841,7 +5790,7 @@ export default function ColorChangeOnClick( exports[`amplify render tests mutations modifies text in component on input change 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5892,7 +5841,7 @@ export default function TwoWayBindings( exports[`amplify render tests mutations supports a controlled checkbox primitive 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5939,7 +5888,7 @@ export default function CheckboxControlledElement( exports[`amplify render tests mutations supports a controlled stepper primitive 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -5989,7 +5938,7 @@ export default function StepperControlledElement( exports[`amplify render tests mutations supports a controlled switch primitive 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6031,7 +5980,7 @@ export default function SwitchControlledElement( exports[`amplify render tests mutations supports all initial value binding types 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, createDataStorePredicate, @@ -6333,7 +6282,7 @@ export default function InitialValueBindings( exports[`amplify render tests mutations supports invalid statement names for mutation targets 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6469,7 +6418,7 @@ var __rest = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps, useStateMutationAction, @@ -6586,7 +6535,7 @@ export default function CardA(props) { exports[`amplify render tests mutations supports multiple actions pointing to the same value 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6644,7 +6593,7 @@ export default function ButtonsToggleState( exports[`amplify render tests mutations supports mutations on synthetic props 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6695,7 +6644,7 @@ export default function MutationWithSyntheticProp( exports[`amplify render tests mutations supports mutations on visibility props 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6754,7 +6703,7 @@ export default function UpdateVisibility( exports[`amplify render tests mutations supports mutations with no initial state 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6816,7 +6765,7 @@ export default function SetStateWithoutInitialValue( exports[`amplify render tests mutations supports names that cant be directly turned into methodnames 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6859,7 +6808,7 @@ export default function InvalidNameForMethod( exports[`amplify render tests mutations supports nested mutation 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -6913,7 +6862,7 @@ export default function NestedMutation( exports[`amplify render tests mutations supports two-way data binding on form elements 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7394,7 +7343,7 @@ export default function TwoWayBindings( exports[`amplify render tests primitives CheckboxField 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7426,7 +7375,7 @@ export default function CheckBoxFieldPrimitive( exports[`amplify render tests primitives Expander 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7470,7 +7419,7 @@ export default function ExpanderPrimitive( exports[`amplify render tests primitives ExpanderItem 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7502,7 +7451,7 @@ export default function MyExpanderItem( exports[`amplify render tests primitives Icon 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7539,7 +7488,7 @@ export default function IconPrimitive( exports[`amplify render tests primitives Menu 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7570,7 +7519,7 @@ export default function MenuPrimitive( exports[`amplify render tests primitives MenuButton 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7600,7 +7549,7 @@ export default function MenuButtonPrimitive( exports[`amplify render tests primitives SliderField 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7635,7 +7584,7 @@ export default function SliderFieldPrimitive( exports[`amplify render tests primitives Table 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7759,7 +7708,7 @@ export default function TablePrimitive( exports[`amplify render tests primitives TextAreaField 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7791,7 +7740,7 @@ export default function TextAreaFieldPrimitive( exports[`amplify render tests primitives TextField 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7824,7 +7773,7 @@ export default function TextFieldPrimitive( exports[`amplify render tests sample code snippet tests should generate a sample code snippet for components 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -7863,7 +7812,7 @@ export default function ViewWithButton( exports[`amplify render tests should render events 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -8013,7 +7962,7 @@ export default function Events(props: EventsProps): React.ReactElement { exports[`amplify render tests should render parsed fixed values 1`] = ` Object { "componentText": "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, @@ -8120,7 +8069,7 @@ exports[`amplify render tests source maps should render inline source maps 1`] = return t; }; /* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { getOverrideProps, useAuth } from \\"@aws-amplify/ui-react/internal\\"; import { Button, Flex, Image } from \\"@aws-amplify/ui-react\\"; export default function Profile(props) { @@ -8160,13 +8109,13 @@ export default function Profile(props) { ) ); } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibW9kdWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibW9kdWxlLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQUFBLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFDMUIsT0FBTyxFQUFvQixnQkFBZ0IsRUFBRSxPQUFPLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBYSxLQUFLLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUt2RSxNQUFNLENBQUMsT0FBTyxVQUFVLE9BQU8sQ0FBQyxLQUFtQjs7SUFDL0MsTUFBTSxFQUFFLFNBQVMsS0FBYyxLQUFLLEVBQWQsSUFBSSxVQUFLLEtBQUssRUFBOUIsYUFBc0IsQ0FBUSxDQUFDO0lBQ3JDLE1BQU0sY0FBYyxHQUFHLE1BQUEsTUFBQSxPQUFPLEVBQUUsQ0FBQyxJQUFJLDBDQUFFLFVBQVUsbUNBQUksRUFBRSxDQUFDO0lBQ3hELE9BQU8sQ0FBQyxvQkFBQyxJQUFJLG9CQUFLLElBQUksRUFBTSxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsU0FBUyxDQUFDO1FBQUUsb0JBQUMsS0FBSyxrQkFBQyxHQUFHLEVBQUUsY0FBYyxDQUFDLFVBQVUsQ0FBQyxJQUFNLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxRQUFRLENBQUMsRUFBVTtRQUFBLG9CQUFDLE1BQU0sa0JBQUMsUUFBUSxFQUFFLGNBQWMsQ0FBQyxTQUFTLENBQUMsSUFBTSxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLEVBQVc7UUFBQSxvQkFBQyxNQUFNLGtCQUFDLFFBQVEsRUFBRSxjQUFjLENBQUMsMEJBQTBCLENBQUMsSUFBTSxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLEVBQVcsQ0FBTyxDQUFDLENBQUM7QUFDL1gsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qIGVzbGludC1kaXNhYmxlICovXG5pbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCI7XG5pbXBvcnQgeyBFc2NhcGVIYXRjaFByb3BzLCBnZXRPdmVycmlkZVByb3BzLCB1c2VBdXRoIH0gZnJvbSBcIkBhd3MtYW1wbGlmeS91aS1yZWFjdC9pbnRlcm5hbFwiO1xuaW1wb3J0IHsgQnV0dG9uLCBGbGV4LCBGbGV4UHJvcHMsIEltYWdlIH0gZnJvbSBcIkBhd3MtYW1wbGlmeS91aS1yZWFjdFwiO1xuXG5leHBvcnQgdHlwZSBQcm9maWxlUHJvcHMgPSBSZWFjdC5Qcm9wc1dpdGhDaGlsZHJlbjxQYXJ0aWFsPEZsZXhQcm9wcz4gJiB7XG4gICAgb3ZlcnJpZGVzPzogRXNjYXBlSGF0Y2hQcm9wcyB8IHVuZGVmaW5lZCB8IG51bGw7XG59PjtcbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIFByb2ZpbGUocHJvcHM6IFByb2ZpbGVQcm9wcyk6IFJlYWN0LlJlYWN0RWxlbWVudCB7XG4gICAgY29uc3QgeyBvdmVycmlkZXMsIC4uLnJlc3QgfSA9IHByb3BzO1xuICAgIGNvbnN0IGF1dGhBdHRyaWJ1dGVzID0gdXNlQXV0aCgpLnVzZXI/LmF0dHJpYnV0ZXMgPz8ge307XG4gICAgcmV0dXJuICg8RmxleCB7Li4ucmVzdH0gey4uLmdldE92ZXJyaWRlUHJvcHMob3ZlcnJpZGVzLCBcIlByb2ZpbGVcIil9PjxJbWFnZSBzcmM9e2F1dGhBdHRyaWJ1dGVzW1widXNlcm5hbWVcIl19IHsuLi5nZXRPdmVycmlkZVByb3BzKG92ZXJyaWRlcywgXCJjaGlsZDFcIil9PjwvSW1hZ2U+PEJ1dHRvbiBjaGlsZHJlbj17YXV0aEF0dHJpYnV0ZXNbXCJwaWN0dXJlXCJdfSB7Li4uZ2V0T3ZlcnJpZGVQcm9wcyhvdmVycmlkZXMsIFwiY2hpbGQyXCIpfT48L0J1dHRvbj48QnV0dG9uIGNoaWxkcmVuPXthdXRoQXR0cmlidXRlc1tcImN1c3RvbTpmYXZvcml0ZV9pY2VjcmVhbVwiXX0gey4uLmdldE92ZXJyaWRlUHJvcHMob3ZlcnJpZGVzLCBcImNoaWxkM1wiKX0+PC9CdXR0b24+PC9GbGV4Pik7XG59Il19 +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibW9kdWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibW9kdWxlLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQUFBLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssS0FBSyxNQUFNLE9BQU8sQ0FBQztBQUMvQixPQUFPLEVBQW9CLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQzdGLE9BQU8sRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFhLEtBQUssRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBS3ZFLE1BQU0sQ0FBQyxPQUFPLFVBQVUsT0FBTyxDQUFDLEtBQW1COztJQUMvQyxNQUFNLEVBQUUsU0FBUyxLQUFjLEtBQUssRUFBZCxJQUFJLFVBQUssS0FBSyxFQUE5QixhQUFzQixDQUFRLENBQUM7SUFDckMsTUFBTSxjQUFjLEdBQUcsTUFBQSxNQUFBLE9BQU8sRUFBRSxDQUFDLElBQUksMENBQUUsVUFBVSxtQ0FBSSxFQUFFLENBQUM7SUFDeEQsT0FBTyxDQUFDLG9CQUFDLElBQUksb0JBQUssSUFBSSxFQUFNLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxTQUFTLENBQUM7UUFBRSxvQkFBQyxLQUFLLGtCQUFDLEdBQUcsRUFBRSxjQUFjLENBQUMsVUFBVSxDQUFDLElBQU0sZ0JBQWdCLENBQUMsU0FBUyxFQUFFLFFBQVEsQ0FBQyxFQUFVO1FBQUEsb0JBQUMsTUFBTSxrQkFBQyxRQUFRLEVBQUUsY0FBYyxDQUFDLFNBQVMsQ0FBQyxJQUFNLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxRQUFRLENBQUMsRUFBVztRQUFBLG9CQUFDLE1BQU0sa0JBQUMsUUFBUSxFQUFFLGNBQWMsQ0FBQywwQkFBMEIsQ0FBQyxJQUFNLGdCQUFnQixDQUFDLFNBQVMsRUFBRSxRQUFRLENBQUMsRUFBVyxDQUFPLENBQUMsQ0FBQztBQUMvWCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyogZXNsaW50LWRpc2FibGUgKi9cbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gXCJyZWFjdFwiO1xuaW1wb3J0IHsgRXNjYXBlSGF0Y2hQcm9wcywgZ2V0T3ZlcnJpZGVQcm9wcywgdXNlQXV0aCB9IGZyb20gXCJAYXdzLWFtcGxpZnkvdWktcmVhY3QvaW50ZXJuYWxcIjtcbmltcG9ydCB7IEJ1dHRvbiwgRmxleCwgRmxleFByb3BzLCBJbWFnZSB9IGZyb20gXCJAYXdzLWFtcGxpZnkvdWktcmVhY3RcIjtcblxuZXhwb3J0IHR5cGUgUHJvZmlsZVByb3BzID0gUmVhY3QuUHJvcHNXaXRoQ2hpbGRyZW48UGFydGlhbDxGbGV4UHJvcHM+ICYge1xuICAgIG92ZXJyaWRlcz86IEVzY2FwZUhhdGNoUHJvcHMgfCB1bmRlZmluZWQgfCBudWxsO1xufT47XG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBQcm9maWxlKHByb3BzOiBQcm9maWxlUHJvcHMpOiBSZWFjdC5SZWFjdEVsZW1lbnQge1xuICAgIGNvbnN0IHsgb3ZlcnJpZGVzLCAuLi5yZXN0IH0gPSBwcm9wcztcbiAgICBjb25zdCBhdXRoQXR0cmlidXRlcyA9IHVzZUF1dGgoKS51c2VyPy5hdHRyaWJ1dGVzID8/IHt9O1xuICAgIHJldHVybiAoPEZsZXggey4uLnJlc3R9IHsuLi5nZXRPdmVycmlkZVByb3BzKG92ZXJyaWRlcywgXCJQcm9maWxlXCIpfT48SW1hZ2Ugc3JjPXthdXRoQXR0cmlidXRlc1tcInVzZXJuYW1lXCJdfSB7Li4uZ2V0T3ZlcnJpZGVQcm9wcyhvdmVycmlkZXMsIFwiY2hpbGQxXCIpfT48L0ltYWdlPjxCdXR0b24gY2hpbGRyZW49e2F1dGhBdHRyaWJ1dGVzW1wicGljdHVyZVwiXX0gey4uLmdldE92ZXJyaWRlUHJvcHMob3ZlcnJpZGVzLCBcImNoaWxkMlwiKX0+PC9CdXR0b24+PEJ1dHRvbiBjaGlsZHJlbj17YXV0aEF0dHJpYnV0ZXNbXCJjdXN0b206ZmF2b3JpdGVfaWNlY3JlYW1cIl19IHsuLi5nZXRPdmVycmlkZVByb3BzKG92ZXJyaWRlcywgXCJjaGlsZDNcIil9PjwvQnV0dG9uPjwvRmxleD4pO1xufSJdfQ== " `; exports[`amplify render tests user specific attributes should render user specific attributes 1`] = ` "/* eslint-disable */ -import React from \\"react\\"; +import * as React from \\"react\\"; import { EscapeHatchProps, getOverrideProps, diff --git a/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts b/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts index 8e8b2aad4..57475bdca 100644 --- a/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts +++ b/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts @@ -13,10 +13,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StudioTemplateRendererFactory, GenericDataSchema, StudioComponent } from '@aws-amplify/codegen-ui'; +import { + StudioTemplateRendererFactory, + GenericDataSchema, + StudioComponent, + getGenericFromDataStore, + Schema, + StudioForm, + StudioView, +} from '@aws-amplify/codegen-ui'; +import { createPrinter, createSourceFile, EmitHint, NewLineKind, Node } from 'typescript'; +import { AmplifyFormRenderer } from '../../amplify-ui-renderers/amplify-form-renderer'; import { AmplifyRenderer } from '../../amplify-ui-renderers/amplify-renderer'; -import { ReactRenderConfig } from '../../react-render-config'; +import { AmplifyViewRenderer } from '../../amplify-ui-renderers/amplify-view-renderer'; +import { ModuleKind, ReactRenderConfig, ScriptKind, ScriptTarget } from '../../react-render-config'; import { loadSchemaFromJSONFile } from './example-schema'; +import { defaultRenderConfig, transpile } from '../../react-studio-template-renderer-helper'; + +export const defaultCLIRenderConfig = { + module: ModuleKind.ES2020, + target: ScriptTarget.ES2020, + script: ScriptKind.JSX, + renderTypeDeclarations: true, +}; export const generateWithAmplifyRenderer = ( jsonSchemaFile: string, @@ -32,3 +51,70 @@ export const generateWithAmplifyRenderer = ( ? { componentText: renderer.renderSampleCodeSnippet().compText } : renderer.renderComponent(); }; + +export const generateWithAmplifyFormRenderer = ( + formJsonFile: string, + dataSchemaJsonFile: string | undefined, + renderConfig: ReactRenderConfig = defaultCLIRenderConfig, +): { componentText: string; declaration?: string } => { + let dataSchema: GenericDataSchema | undefined; + if (dataSchemaJsonFile) { + const dataStoreSchema = loadSchemaFromJSONFile(dataSchemaJsonFile); + dataSchema = getGenericFromDataStore(dataStoreSchema); + } + const rendererFactory = new StudioTemplateRendererFactory( + (component: StudioForm) => new AmplifyFormRenderer(component, dataSchema, renderConfig), + ); + + const renderer = rendererFactory.buildRenderer(loadSchemaFromJSONFile(formJsonFile)); + return renderer.renderComponent(); +}; + +export const renderWithAmplifyViewRenderer = ( + viewJsonFile: string, + dataSchemaJsonFile: string | undefined, + renderConfig: ReactRenderConfig = defaultCLIRenderConfig, +): { componentText: string; declaration?: string } => { + let dataSchema: GenericDataSchema | undefined; + if (dataSchemaJsonFile) { + const dataStoreSchema = loadSchemaFromJSONFile(dataSchemaJsonFile); + dataSchema = getGenericFromDataStore(dataStoreSchema); + } + const rendererFactory = new StudioTemplateRendererFactory( + (view: StudioView) => new AmplifyViewRenderer(view, dataSchema, renderConfig), + ); + + const renderer = rendererFactory.buildRenderer(loadSchemaFromJSONFile(viewJsonFile)); + return renderer.renderComponent(); +}; + +export const renderTableJsxElement = ( + tableFilePath: string, + dataSchemaFilePath: string | undefined, + snapshotFileName: string, + renderConfig: ReactRenderConfig = defaultCLIRenderConfig, +): string => { + const table = loadSchemaFromJSONFile(tableFilePath); + const dataSchema = dataSchemaFilePath ? loadSchemaFromJSONFile(dataSchemaFilePath) : undefined; + const tableJsx = new AmplifyViewRenderer(table, dataSchema, renderConfig).renderJsx(); + + const file = createSourceFile(snapshotFileName, '', ScriptTarget.ES2015, true, ScriptKind.TS); + const printer = createPrinter(); + const tableNode = printer.printNode(EmitHint.Unspecified, tableJsx, file); + + return transpile(tableNode, {}).componentText; +}; + +export const genericPrinter = (node: Node): string => { + const file = createSourceFile( + 'sampleFileName.js', + '', + defaultCLIRenderConfig.target, + false, + defaultRenderConfig.script, + ); + const printer = createPrinter({ + newLine: NewLineKind.LineFeed, + }); + return printer.printNode(EmitHint.Unspecified, node, file); +}; diff --git a/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/form-state.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/form-state.test.ts.snap new file mode 100644 index 000000000..112ae35b1 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/form-state.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`nested state should generate state structure for nested keyPath 1`] = `"{ ...bio, favoriteAnimal: { ...bio?.favoriteAnimal, animalMeta: { ...bio?.favoriteAnimal?.animalMeta, family: { ...bio?.favoriteAnimal?.animalMeta?.family, genus: value } } } }"`; + +exports[`nested state should generate value for 2nd level deep object 1`] = `"{ ...bio, firstName: \\"John C\\" }"`; + +exports[`set field state should generate state call for nested object 1`] = `"setBio({ ...bio, favoriteAnimal: { ...bio?.favoriteAnimal, animalMeta: { ...bio?.favoriteAnimal?.animalMeta, family: { ...bio?.favoriteAnimal?.animalMeta?.family, genus: \\"hello World\\" } } } })"`; + +exports[`set field state should generate state call for non-nested objects 1`] = `"setFirstName(\\"john c\\")"`; diff --git a/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/type-helper.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/type-helper.test.ts.snap new file mode 100644 index 000000000..ea232bcf4 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/forms/__snapshots__/type-helper.test.ts.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should generate nested object should generate type for nested object 1`] = ` +"export declare type myCreateFormInputValues = { + firstName?: UseBaseOrValidationType; + isExplorer?: UseBaseOrValidationType; + bio?: { + favoriteAnimal?: { + animalMeta?: { + family?: { + genus?: UseBaseOrValidationType; + }; + earliestRecord?: UseBaseOrValidationType; + }; + }; + }; +};" +`; + +exports[`should generate nested object should generate type for non nested object 1`] = ` +"export declare type myCreateFormInputValues = { + firstName?: UseBaseOrValidationType; + isExplorer?: UseBaseOrValidationType; + tags?: UseBaseOrValidationType; +};" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts b/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts new file mode 100644 index 000000000..7f8896234 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts @@ -0,0 +1,59 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { factory } from 'typescript'; +import { buildNestedStateSet, setFieldState } from '../../forms/form-state'; +import { genericPrinter } from '../__utils__'; + +describe('nested state', () => { + it('should generate state structure for nested keyPath', () => { + const state = buildNestedStateSet( + ['bio', 'favoriteAnimal', 'animalMeta', 'family', 'genus'], + ['bio'], + factory.createIdentifier('value'), + ); + const response = genericPrinter(state); + expect(response).toMatchSnapshot(); + }); + + it('should generate value for 2nd level deep object', () => { + const state = buildNestedStateSet(['bio', 'firstName'], ['bio'], factory.createStringLiteral('John C')); + const response = genericPrinter(state); + expect(response).toMatchSnapshot(); + }); + + it('should throw error for 1 level deep path', () => { + expect(() => buildNestedStateSet(['firstName'], ['firstName'], factory.createStringLiteral('John C'))).toThrowError( + 'keyPath needs a length larger than 1 to build nested state object', + ); + }); +}); + +describe('set field state', () => { + it('should generate state call for nested object', () => { + const fieldStateSetter = setFieldState( + 'bio.favoriteAnimal.animalMeta.family.genus', + factory.createStringLiteral('hello World'), + ); + const response = genericPrinter(fieldStateSetter); + expect(response).toMatchSnapshot(); + }); + + it('should generate state call for non-nested objects', () => { + const fieldStateSetter = setFieldState('firstName', factory.createStringLiteral('john c')); + const response = genericPrinter(fieldStateSetter); + expect(response).toMatchSnapshot(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/forms/type-helper.test.ts b/packages/codegen-ui-react/lib/__tests__/forms/type-helper.test.ts new file mode 100644 index 000000000..3fe7ab9c1 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/forms/type-helper.test.ts @@ -0,0 +1,72 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { FieldConfigMetadata } from '@aws-amplify/codegen-ui'; +import { generateInputTypes } from '../../forms/type-helper'; +import { genericPrinter } from '../__utils__'; + +describe('should generate nested object', () => { + it('should generate type for nested object', () => { + const fieldConfigs: Record = { + 'bio.favoriteAnimal.animalMeta.family.genus': { + dataType: 'String', + validationRules: [], + componentType: 'TextField', + }, + 'bio.favoriteAnimal.animalMeta.earliestRecord': { + dataType: 'AWSTimestamp', + validationRules: [], + componentType: 'TextField', + }, + firstName: { + dataType: 'String', + validationRules: [], + componentType: 'TextField', + }, + isExplorer: { + dataType: 'Boolean', + validationRules: [], + componentType: 'CheckboxField', + }, + }; + const types = generateInputTypes('myCreateForm', fieldConfigs); + const response = genericPrinter(types); + expect(response).toMatchSnapshot(); + }); + + it('should generate type for non nested object', () => { + const fieldConfigs: Record = { + firstName: { + dataType: 'String', + validationRules: [], + componentType: 'TextField', + }, + isExplorer: { + dataType: 'Boolean', + validationRules: [], + componentType: 'RadioGroupField', + }, + tags: { + validationRules: [], + componentType: 'TextField', + isArray: true, + }, + }; + const types = generateInputTypes('myCreateForm', fieldConfigs); + const response = genericPrinter(types); + expect(response).toMatchSnapshot(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/forms/validation.test.ts b/packages/codegen-ui-react/lib/__tests__/forms/validation.test.ts index 7c64af0af..2f5b20fb9 100644 --- a/packages/codegen-ui-react/lib/__tests__/forms/validation.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/forms/validation.test.ts @@ -13,14 +13,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ValidationTypes } from '@aws-amplify/codegen-ui/lib/types/form/form-validation'; +import { FieldValidationConfiguration, ValidationTypes } from '@aws-amplify/codegen-ui/lib/types/form/form-validation'; import { validateField } from '../../utils/forms/validation'; describe('validateField tests', () => { it('should validate REQUIRED type', () => { expect(validateField('123', [{ type: ValidationTypes.REQUIRED, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value is required', }); expect(validateField('', [{ type: ValidationTypes.REQUIRED, validationMessage: '' }])).toEqual({ hasError: true, @@ -28,115 +27,130 @@ describe('validateField tests', () => { }); expect(validateField(0, [{ type: ValidationTypes.REQUIRED, validationMessage: 'test' }])).toEqual({ hasError: false, - errorMessage: 'test', }); }); it('should validate START_WITH type', () => { - expect(validateField('abc', [{ type: ValidationTypes.START_WITH, values: [], validationMessage: 'test' }])).toEqual( - { hasError: true, errorMessage: 'test' }, - ); - expect(validateField('abc', [{ type: ValidationTypes.START_WITH, values: ['4'], validationMessage: '' }])).toEqual({ + expect( + validateField('abc', [{ type: ValidationTypes.START_WITH, strValues: [], validationMessage: 'test' }]), + ).toEqual({ hasError: false }); + expect( + validateField('abc', [{ type: ValidationTypes.START_WITH, strValues: ['4'], validationMessage: '' }]), + ).toEqual({ hasError: true, errorMessage: 'The value must start with 4', }); expect( - validateField('aardvark', [{ type: ValidationTypes.START_WITH, values: ['a', 'b'], validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: 'The value must start with a, b' }); + validateField('aardvark', [{ type: ValidationTypes.START_WITH, strValues: ['a', 'b'], validationMessage: '' }]), + ).toEqual({ hasError: false }); }); it('should validate END_WITH type', () => { - expect(validateField('abc', [{ type: ValidationTypes.END_WITH, values: ['c'], validationMessage: '' }])).toEqual({ + expect( + validateField('abc', [{ type: ValidationTypes.END_WITH, strValues: ['c'], validationMessage: 'test' }]), + ).toEqual({ hasError: false, - errorMessage: 'The value must end with c', }); expect( - validateField('abc', [{ type: ValidationTypes.END_WITH, values: ['e', 'f'], validationMessage: '' }]), + validateField('abc', [{ type: ValidationTypes.END_WITH, strValues: ['e', 'f'], validationMessage: '' }]), ).toEqual({ hasError: true, errorMessage: 'The value must end with e, f' }); - expect(validateField('', [{ type: ValidationTypes.END_WITH, values: [], validationMessage: 'test' }])).toEqual({ - hasError: true, - errorMessage: 'test', + expect(validateField('', [{ type: ValidationTypes.END_WITH, strValues: [], validationMessage: 'test' }])).toEqual({ + hasError: false, }); }); it('should validate CONTAINS type', () => { - expect(validateField('abc', [{ type: ValidationTypes.CONTAINS, values: ['a'], validationMessage: '' }])).toEqual({ - hasError: false, - errorMessage: 'The value must contain a', - }); + expect(validateField('abc', [{ type: ValidationTypes.CONTAINS, strValues: ['a'], validationMessage: '' }])).toEqual( + { + hasError: false, + }, + ); expect( - validateField('abcd', [{ type: ValidationTypes.CONTAINS, values: ['a', 'e'], validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: 'The value must contain a, e' }); + validateField('abcd', [{ type: ValidationTypes.CONTAINS, strValues: ['a', 'e'], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField('abc', [{ type: ValidationTypes.CONTAINS, values: ['d'], validationMessage: 'test' }]), + validateField('abc', [{ type: ValidationTypes.CONTAINS, strValues: ['d'], validationMessage: 'test' }]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate NOT_CONTAINS type', () => { expect( - validateField('abc', [{ type: ValidationTypes.NOT_CONTAINS, values: ['4'], validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: 'The value must not contain 4' }); + validateField('abc', [{ type: ValidationTypes.NOT_CONTAINS, strValues: ['4'], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField('abc', [{ type: ValidationTypes.NOT_CONTAINS, values: ['d', 'a'], validationMessage: '' }]), + validateField('abc', [{ type: ValidationTypes.NOT_CONTAINS, strValues: ['d', 'a'], validationMessage: '' }]), ).toEqual({ hasError: true, errorMessage: 'The value must not contain d, a' }); - expect(validateField('', [{ type: ValidationTypes.NOT_CONTAINS, values: [], validationMessage: 'test' }])).toEqual({ + expect( + validateField('', [{ type: ValidationTypes.NOT_CONTAINS, strValues: [], validationMessage: 'test' }]), + ).toEqual({ hasError: false, - errorMessage: 'test', }); }); it('should validate LESS_THAN_CHAR_LENGTH type', () => { expect( - validateField('123', [{ type: ValidationTypes.LESS_THAN_CHAR_LENGTH, values: 4, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: 'The value must be shorter than 4' }); + validateField('123', [{ type: ValidationTypes.LESS_THAN_CHAR_LENGTH, numValues: [4], validationMessage: '' }]), + ).toEqual({ hasError: false }); + expect( + validateField('', [{ type: ValidationTypes.LESS_THAN_CHAR_LENGTH, numValues: [3], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField('', [{ type: ValidationTypes.LESS_THAN_CHAR_LENGTH, values: 0, validationMessage: '' }]), - ).toEqual({ hasError: true, errorMessage: 'The value must be shorter than 0' }); + validateField('23445', [{ type: ValidationTypes.LESS_THAN_CHAR_LENGTH, numValues: [3], validationMessage: '' }]), + ).toEqual({ hasError: true, errorMessage: 'The value must be shorter than 3' }); }); it('should validate GREATER_THAN_CHAR_LENGTH type', () => { expect( - validateField('123', [{ type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, values: 0, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: 'The value must be longer than 0' }); + validateField('123', [{ type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, numValues: [0], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField('', [{ type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, values: 0, validationMessage: '' }]), - ).toEqual({ hasError: true, errorMessage: 'The value must be longer than 0' }); + validateField('', [{ type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, numValues: [3], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField('', [{ type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, values: 1, validationMessage: 'test' }]), + validateField('df', [ + { type: ValidationTypes.GREATER_THAN_CHAR_LENGTH, numValues: [3], validationMessage: 'test' }, + ]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate LESS_THAN_NUM type', () => { - expect(validateField(1, [{ type: ValidationTypes.LESS_THAN_NUM, values: 10, validationMessage: '' }])).toEqual({ - hasError: false, - errorMessage: 'The value must be less than 10', - }); - expect(validateField(2, [{ type: ValidationTypes.LESS_THAN_NUM, values: 1, validationMessage: '' }])).toEqual({ + expect(validateField(1, [{ type: ValidationTypes.LESS_THAN_NUM, numValues: [10], validationMessage: '' }])).toEqual( + { + hasError: false, + }, + ); + expect(validateField(2, [{ type: ValidationTypes.LESS_THAN_NUM, numValues: [1], validationMessage: '' }])).toEqual({ hasError: true, errorMessage: 'The value must be less than 1', }); - expect(validateField(0, [{ type: ValidationTypes.LESS_THAN_NUM, values: 0, validationMessage: 'test' }])).toEqual({ + expect( + validateField(0, [{ type: ValidationTypes.LESS_THAN_NUM, numValues: [0], validationMessage: 'test' }]), + ).toEqual({ hasError: true, errorMessage: 'test', }); }); it('should validate GREATER_THAN_NUM type', () => { - expect(validateField(1, [{ type: ValidationTypes.GREATER_THAN_NUM, values: 0, validationMessage: '' }])).toEqual({ + expect( + validateField(1, [{ type: ValidationTypes.GREATER_THAN_NUM, numValues: [0], validationMessage: '' }]), + ).toEqual({ hasError: false, - errorMessage: 'The value must be greater than 0', }); - expect(validateField(2, [{ type: ValidationTypes.GREATER_THAN_NUM, values: 3, validationMessage: '' }])).toEqual({ + expect( + validateField(2, [{ type: ValidationTypes.GREATER_THAN_NUM, numValues: [3], validationMessage: '' }]), + ).toEqual({ hasError: true, errorMessage: 'The value must be greater than 3', }); expect( - validateField(3, [{ type: ValidationTypes.GREATER_THAN_NUM, values: 3, validationMessage: 'test' }]), + validateField(3, [{ type: ValidationTypes.GREATER_THAN_NUM, numValues: [3], validationMessage: 'test' }]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate EQUAL_TO_NUM type', () => { - expect(validateField(1, [{ type: ValidationTypes.EQUAL_TO_NUM, values: [1, 2], validationMessage: '' }])).toEqual({ + expect( + validateField(1, [{ type: ValidationTypes.EQUAL_TO_NUM, numValues: [1, 2], validationMessage: '' }]), + ).toEqual({ hasError: false, - errorMessage: 'The value must be equal to 1 or 2', }); - expect(validateField(2, [{ type: ValidationTypes.EQUAL_TO_NUM, values: 3, validationMessage: '' }])).toEqual({ + expect(validateField(2, [{ type: ValidationTypes.EQUAL_TO_NUM, numValues: [3], validationMessage: '' }])).toEqual({ hasError: true, errorMessage: 'The value must be equal to 3', }); expect( - validateField(3, [{ type: ValidationTypes.EQUAL_TO_NUM, values: [4, 5, 6], validationMessage: 'test' }]), + validateField(3, [{ type: ValidationTypes.EQUAL_TO_NUM, numValues: [4, 5, 6], validationMessage: 'test' }]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate BE_AFTER type', () => { @@ -144,25 +158,31 @@ describe('validateField tests', () => { const endDate1 = new Date('2021-01-09').toDateString(); const endDate2 = new Date('3000-01-09').toDateString(); expect( - validateField(startDate, [{ type: ValidationTypes.BE_AFTER, values: endDate1, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: `The value must be after ${endDate1}` }); + validateField(startDate, [{ type: ValidationTypes.BE_AFTER, strValues: [endDate1], validationMessage: '' }]), + ).toEqual({ hasError: false }); expect( - validateField(startDate, [{ type: ValidationTypes.BE_AFTER, values: endDate2, validationMessage: '' }]), + validateField(startDate, [{ type: ValidationTypes.BE_AFTER, strValues: [endDate2], validationMessage: '' }]), ).toEqual({ hasError: true, errorMessage: `The value must be after ${endDate2}` }); expect( - validateField(startDate, [{ type: ValidationTypes.BE_AFTER, values: '', validationMessage: 'test' }]), + validateField(startDate, [{ type: ValidationTypes.BE_AFTER, strValues: [''], validationMessage: 'test' }]), ).toEqual({ hasError: true, errorMessage: 'test' }); const startTime = Date.now(); const endTime1 = startTime - 10; expect( - validateField(startTime, [{ type: ValidationTypes.BE_AFTER, values: endTime1, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: `The value must be after ${endTime1}` }); + validateField(startTime, [ + { type: ValidationTypes.BE_AFTER, strValues: [endTime1.toString()], validationMessage: '' }, + ]), + ).toEqual({ hasError: false }); expect( - validateField(endTime1, [{ type: ValidationTypes.BE_AFTER, values: startTime, validationMessage: '' }]), + validateField(endTime1, [ + { type: ValidationTypes.BE_AFTER, strValues: [startTime.toString()], validationMessage: '' }, + ]), ).toEqual({ hasError: true, errorMessage: `The value must be after ${startTime}` }); expect( - validateField(endTime1, [{ type: ValidationTypes.BE_AFTER, values: startTime, validationMessage: 'test' }]), + validateField(endTime1, [ + { type: ValidationTypes.BE_AFTER, strValues: [startTime.toString()], validationMessage: 'test' }, + ]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate BE_BEFORE type', () => { @@ -171,31 +191,42 @@ describe('validateField tests', () => { const endDate2 = new Date('2021-01-09').toString(); expect( - validateField(startDate, [{ type: ValidationTypes.BE_BEFORE, values: endDate1, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: `The value must be before ${endDate1}` }); + validateField(startDate, [ + { type: ValidationTypes.BE_BEFORE, strValues: [endDate1.toString()], validationMessage: '' }, + ]), + ).toEqual({ hasError: false }); expect( - validateField(startDate, [{ type: ValidationTypes.BE_BEFORE, values: endDate2, validationMessage: '' }]), + validateField(startDate, [ + { type: ValidationTypes.BE_BEFORE, strValues: [endDate2.toString()], validationMessage: '' }, + ]), ).toEqual({ hasError: true, errorMessage: `The value must be before ${endDate2}` }); expect( - validateField(startDate, [{ type: ValidationTypes.BE_BEFORE, values: endDate2, validationMessage: 'test' }]), + validateField(startDate, [ + { type: ValidationTypes.BE_BEFORE, strValues: [endDate2.toString()], validationMessage: 'test' }, + ]), ).toEqual({ hasError: true, errorMessage: 'test' }); const startTime = Date.now(); const endTime1 = startTime + 10; expect( - validateField(startTime, [{ type: ValidationTypes.BE_BEFORE, values: endTime1, validationMessage: '' }]), - ).toEqual({ hasError: false, errorMessage: `The value must be before ${endTime1}` }); + validateField(startTime, [ + { type: ValidationTypes.BE_BEFORE, strValues: [endTime1.toString()], validationMessage: '' }, + ]), + ).toEqual({ hasError: false }); expect( - validateField(endTime1, [{ type: ValidationTypes.BE_BEFORE, values: startTime, validationMessage: '' }]), + validateField(endTime1, [ + { type: ValidationTypes.BE_BEFORE, strValues: [startTime.toString()], validationMessage: '' }, + ]), ).toEqual({ hasError: true, errorMessage: `The value must be before ${startTime}` }); expect( - validateField(endTime1, [{ type: ValidationTypes.BE_BEFORE, values: startTime, validationMessage: 'test' }]), + validateField(endTime1, [ + { type: ValidationTypes.BE_BEFORE, strValues: [startTime.toString()], validationMessage: 'test' }, + ]), ).toEqual({ hasError: true, errorMessage: 'test' }); }); it('should validate EMAIL type', () => { expect(validateField('ab-cd@amazon.com', [{ type: ValidationTypes.EMAIL, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be a valid email address', }); expect(validateField('@amazon.com', [{ type: ValidationTypes.EMAIL, validationMessage: '' }])).toEqual({ hasError: true, @@ -209,48 +240,41 @@ describe('validateField tests', () => { it('should validate JSON type', () => { expect(validateField('{}', [{ type: ValidationTypes.JSON, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be in a correct JSON format', }); expect(validateField('{"name": "test"}', [{ type: ValidationTypes.JSON, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be in a correct JSON format', }); - expect(validateField('\\\\', [{ type: ValidationTypes.JSON, validationMessage: '' }])).toEqual({ + expect(validateField('\\\\', [{ type: ValidationTypes.JSON, validationMessage: 'test' }])).toEqual({ hasError: true, - errorMessage: 'The value must be in a correct JSON format', + errorMessage: 'test', }); expect(validateField('', [{ type: ValidationTypes.JSON, validationMessage: 'test' }])).toEqual({ - hasError: true, - errorMessage: 'test', + hasError: false, }); }); it('should validate IP_ADDRESS type', () => { expect(validateField('192.168.1.1', [{ type: ValidationTypes.IP_ADDRESS, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be an IPv4 or IPv6 address', }); expect( validateField('2001:0db8:85a3:0000:0000:8a2e:0370:7334', [ { type: ValidationTypes.IP_ADDRESS, validationMessage: '' }, ]), - ).toEqual({ hasError: false, errorMessage: 'The value must be an IPv4 or IPv6 address' }); - expect(validateField('1.1', [{ type: ValidationTypes.IP_ADDRESS, validationMessage: '' }])).toEqual({ + ).toEqual({ hasError: false }); + expect(validateField('1.1', [{ type: ValidationTypes.IP_ADDRESS, validationMessage: 'test' }])).toEqual({ hasError: true, - errorMessage: 'The value must be an IPv4 or IPv6 address', + errorMessage: 'test', }); expect(validateField('', [{ type: ValidationTypes.IP_ADDRESS, validationMessage: 'test' }])).toEqual({ - hasError: true, - errorMessage: 'test', + hasError: false, }); }); it('should validate URL type', () => { expect(validateField('http://amazon.com', [{ type: ValidationTypes.URL, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be a valid URL that begins with a schema (i.e. http:// or mailto:)', }); expect(validateField('mailto:amazon.com', [{ type: ValidationTypes.URL, validationMessage: '' }])).toEqual({ hasError: false, - errorMessage: 'The value must be a valid URL that begins with a schema (i.e. http:// or mailto:)', }); expect(validateField('.amazon.com', [{ type: ValidationTypes.URL, validationMessage: '' }])).toEqual({ hasError: true, @@ -261,4 +285,46 @@ describe('validateField tests', () => { errorMessage: 'test', }); }); + + it('should validate Phone type', () => { + expect(validateField('kdj34324', [{ type: ValidationTypes.PHONE }])).toEqual({ + hasError: true, + errorMessage: 'The value must be a valid phone number', + }); + + expect(validateField('2938493029', [{ type: ValidationTypes.PHONE, validationMessage: 'test' }])).toEqual({ + hasError: false, + }); + + expect(validateField('293 849 3029', [{ type: ValidationTypes.PHONE, validationMessage: 'test' }])).toEqual({ + hasError: false, + }); + + expect(validateField('293-849-3029', [{ type: ValidationTypes.PHONE, validationMessage: 'test' }])).toEqual({ + hasError: false, + }); + + expect(validateField('293 849-3029', [{ type: ValidationTypes.PHONE, validationMessage: 'test' }])).toEqual({ + hasError: false, + }); + }); + + it('should test value against all configured validations', () => { + const validationList: FieldValidationConfiguration[] = [ + { type: ValidationTypes.START_WITH, strValues: ['he'], validationMessage: 'startFailed' }, + { type: ValidationTypes.END_WITH, strValues: ['o'], validationMessage: 'endFailed' }, + ]; + + expect(validateField('hello', validationList)).toEqual({ + hasError: false, + }); + expect(validateField('hey', validationList)).toEqual({ + hasError: true, + errorMessage: 'endFailed', + }); + expect(validateField('yes', validationList)).toEqual({ + hasError: true, + errorMessage: 'startFailed', + }); + }); }); diff --git a/packages/codegen-ui-react/lib/__tests__/imports/__snapshots__/import-collection.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/imports/__snapshots__/import-collection.test.ts.snap index 27694a5ee..388650ff3 100644 --- a/packages/codegen-ui-react/lib/__tests__/imports/__snapshots__/import-collection.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/imports/__snapshots__/import-collection.test.ts.snap @@ -1,43 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ImportCollection addImport multiple identical imports 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Text } from \\"@aws-amplify/ui-react\\";" `; exports[`ImportCollection addImport multiple imports 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Text } from \\"@aws-amplify/ui-react\\"; import { User } from \\"../models\\";" `; exports[`ImportCollection addImport multiple imports from the same package 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Text, getOverrideProps } from \\"@aws-amplify/ui-react\\";" `; exports[`ImportCollection addImport one import 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Text } from \\"@aws-amplify/ui-react\\";" `; exports[`ImportCollection addImport one relative import 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { User } from \\"../models\\";" `; exports[`ImportCollection buildImportStatements multiple imports 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Button, getOverrideProps } from \\"@aws-amplify/ui-react\\"; import { User } from \\"../models\\";" `; -exports[`ImportCollection buildImportStatements no imports 1`] = `"import React from \\"react\\";"`; +exports[`ImportCollection buildImportStatements no imports 1`] = `"import * as React from \\"react\\";"`; exports[`ImportCollection buildSampleSnippetImports 1`] = `"import { MyButton } from \\"./ui-components\\";"`; exports[`ImportCollection mergeCollections 1`] = ` -"import React from \\"react\\"; +"import * as React from \\"react\\"; import { Button, Text, getOverrideProps } from \\"@aws-amplify/ui-react\\"; import { User } from \\"../models\\";" `; diff --git a/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap new file mode 100644 index 000000000..38480e8c8 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/react-forms/__snapshots__/form-renderer-helper.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`form-render utils should generate before & complete types if datastore config is set 1`] = ` +"{ + onSubmit?: (fields: mySampleFormInputValues) => mySampleFormInputValues; + onSuccess?: (fields: mySampleFormInputValues) => void; + onError?: (fields: mySampleFormInputValues, errorMessage: string) => void; + onCancel?: () => void; + onChange?: (fields: mySampleFormInputValues) => mySampleFormInputValues; + onValidate?: mySampleFormInputValues; +}" +`; + +exports[`form-render utils should generate regular onsubmit if dataSourceType is custom 1`] = ` +"{ + onSubmit: (fields: myCustomFormInputValues) => void; + onCancel?: () => void; + onChange?: (fields: myCustomFormInputValues) => myCustomFormInputValues; + onValidate?: myCustomFormInputValues; +}" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts b/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts new file mode 100644 index 000000000..2edb7b4cc --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/react-forms/form-renderer-helper.test.ts @@ -0,0 +1,63 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { StudioForm } from '@aws-amplify/codegen-ui'; +import { EmitHint, Node } from 'typescript'; +import { buildFormPropNode } from '../../forms'; +import { buildPrinter, defaultRenderConfig } from '../../react-studio-template-renderer-helper'; + +describe('form-render utils', () => { + let printNode: (node: Node) => string; + + beforeAll(() => { + const { printer, file } = buildPrinter('myFileMock', defaultRenderConfig); + printNode = (node: Node) => { + return printer.printNode(EmitHint.Unspecified, node, file); + }; + }); + + it('should generate before & complete types if datastore config is set', () => { + const form: StudioForm = { + id: '123', + name: 'mySampleForm', + formActionType: 'create', + dataType: { dataSourceType: 'DataStore', dataTypeName: 'Post' }, + fields: {}, + sectionalElements: {}, + style: {}, + cta: {}, + }; + + const propSignatures = buildFormPropNode(form); + const node = printNode(propSignatures); + expect(node).toMatchSnapshot(); + }); + + it('should generate regular onsubmit if dataSourceType is custom', () => { + const form: StudioForm = { + id: '123', + name: 'myCustomForm', + formActionType: 'create', + dataType: { dataSourceType: 'Custom', dataTypeName: 'Custom' }, + fields: {}, + sectionalElements: {}, + style: {}, + cta: {}, + }; + const propSignatures = buildFormPropNode(form); + const node = printNode(propSignatures); + expect(node).toMatchSnapshot(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts new file mode 100644 index 000000000..103158242 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts @@ -0,0 +1,107 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { generateWithAmplifyFormRenderer } from './__utils__'; + +describe('amplify form renderer tests', () => { + describe('datastore form tests', () => { + it('should generate a create form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/post-datastore-create', + 'datastore/post', + ); + expect(componentText).toContain('DataStore.save'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render form with a two inputs in row', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/post-datastore-create-row', + 'datastore/post', + ); + expect(componentText).toContain('DataStore.save'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should generate a update form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/post-datastore-update', + 'datastore/post', + ); + expect(componentText).toContain('DataStore.save'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render a form with multiple date types', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/input-gallery-create', + 'datastore/input-gallery', + ); + expect(componentText).toContain('DataStore.save'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render a form with multiple date types', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/input-gallery-update', + 'datastore/input-gallery', + ); + expect(componentText).toContain('DataStore.save'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + }); + + describe('custom form tests', () => { + it('should render a custom backed form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/post-custom-create', undefined); + expect(componentText.replace(/\s/g, '')).toContain('onSubmit'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render a custom backed form', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/post-custom-update', undefined); + expect(componentText.replace(/\s/g, '')).toContain('onSubmit'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render sectional elements', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer( + 'forms/custom-with-sectional-elements', + undefined, + ); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render nested json fields', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/bio-nested-create', undefined); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + it('should render nested json fields', () => { + const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/bio-nested-update', undefined); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts new file mode 100644 index 000000000..ac36b8c38 --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts @@ -0,0 +1,56 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { renderTableJsxElement, renderWithAmplifyViewRenderer } from './__utils__'; + +describe('amplify table renderer tests', () => { + test('should generate a table element', () => { + const tableElement = renderTableJsxElement('views/table-from-datastore', 'datastore/person', 'test-table.ts'); + expect(tableElement).toMatchSnapshot(); + }); + + test('should generate a non-datastore table element', () => { + const tableElement = renderTableJsxElement('views/table-from-custom-json', undefined, 'test-custom-table.ts'); + expect(tableElement).toMatchSnapshot(); + }); +}); + +describe('amplify view renderer tests', () => { + test('should render view with passed in predicate and sort', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer( + 'views/post-table-datastore', + 'datastore/post-ds', + ); + expect(componentText).toContain('useDataStoreBinding'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + test('should render view with custom datastore', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer('views/table-from-custom-json', undefined); + expect(componentText).not.toContain('useDataStoreBinding'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + test('should call util file if rendered', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer( + 'views/post-table-custom-format', + 'datastore/post-ds', + ); + expect(componentText.replace(/\\/g, '')).toContain(`import { formatter } from "./utils"`); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react.test.ts index 56c55d80f..ca62e247d 100644 --- a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react.test.ts @@ -49,6 +49,7 @@ describe('amplify render tests', () => { const generatedCode = generateWithAmplifyRenderer('buttonGolden'); expect(generatedCode.componentText.includes('restProp')).toBe(false); expect(generatedCode.componentText.includes('breakpointHook')).toBe(false); + expect(generatedCode.componentText.includes('breakpoint: breakpointHook')).toBe(false); }); }); @@ -239,10 +240,15 @@ describe('amplify render tests', () => { expect(generatedCode).toMatchSnapshot(); }); - it('should have variant specific generation', () => { - const generatedCode = generateWithAmplifyRenderer('componentWithVariants'); - expect(generatedCode.componentText.includes('restProp')).toBe(true); - expect(generatedCode.componentText.includes('breakpointHook')).toBe(true); + it('should have breakpoint specific generation', () => { + const { componentText } = generateWithAmplifyRenderer('componentWithBreakpoint'); + expect(componentText).toContain('restProp'); + expect(componentText).toContain('breakpointHook'); + expect(componentText).toContain('breakpoint: breakpointHook'); + expect(componentText).toContain('base: "small",'); + expect(componentText).toContain('small: "small",'); + expect(componentText).toContain('medium: "medium",'); + expect(componentText).not.toContain('large: "large"'); }); }); diff --git a/packages/codegen-ui-react/lib/__tests__/utils/__snapshots__/string-formatter.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/utils/__snapshots__/string-formatter.test.ts.snap new file mode 100644 index 000000000..206d4409e --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/utils/__snapshots__/string-formatter.test.ts.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`string formatter generateFormatUtil 1`] = ` +"const monthToShortMon: { + [mon: string]: string; +} = { + \\"1\\": \\"Jan\\", + \\"2\\": \\"Feb\\", + \\"3\\": \\"Mar\\", + \\"4\\": \\"Apr\\", + \\"5\\": \\"May\\", + \\"6\\": \\"Jun\\", + \\"7\\": \\"Jul\\", + \\"8\\": \\"Aug\\", + \\"9\\": \\"Sep\\", + \\"10\\": \\"Oct\\", + \\"11\\": \\"Nov\\", + \\"12\\": \\"Dec\\" +}; +const invalidDateStr = \\"Invalid Date\\"; +type DateFormatType = { + type: \\"DateFormat\\"; + format: DateFormat[\\"dateFormat\\"]; +}; +type DateTimeFormatType = { + type: \\"DateTimeFormat\\"; + format: DateTimeFormat[\\"dateTimeFormat\\"]; +}; +type TimeFormatType = { + type: \\"TimeFormat\\"; + format: TimeFormat[\\"timeFormat\\"]; +}; +type FormatInputType = DateFormatType | DateTimeFormatType | TimeFormatType; +export function formatDate(date: string, dateFormat: DateFormat[\\"dateFormat\\"]): string { + if (date === undefined || date === null) { + return date; + } + const validDate = new Date(Date.parse(date)); + if (validDate.toString() === invalidDateStr) { + return date; + } + const splitDate = date.split(/-|\\\\+|Z/); + const year = splitDate[0]; + const month = splitDate[1]; + const day = splitDate[2]; + const truncatedMonth = month.replace(/^0+/, \\"\\"); + switch (dateFormat) { + case \\"locale\\": return validDate.toLocaleDateString(); + case \\"YYYY.MM.DD\\": return \`\${year}.\${month}.\${day}\`; + case \\"DD.MM.YYYY\\": return \`\${day}.\${month}.\${year}\`; + case \\"MM/DD/YYYY\\": return \`\${month}/\${day}/\${year}\`; + case \\"Mmm DD, YYYY\\": return \`\${monthToShortMon[truncatedMonth]} \${day}, \${year}\`; + default: return date; + } +} +export function formatTime(time: string, timeFormat: TimeFormat[\\"timeFormat\\"]): string { + if (time === undefined || time === null) { + return time; + } + const splitTime = time.split(/:|Z/); + if (splitTime.length < 3) { + return time; + } + const validTime = new Date(); + validTime.setHours(Number.parseInt(splitTime[0], 10)); + validTime.setMinutes(Number.parseInt(splitTime[1], 10)); + const splitSeconds = splitTime[2].split(\\".\\"); + validTime.setSeconds(Number.parseInt(splitSeconds[0], 10), Number.parseInt(splitSeconds[1], 10)); + if (validTime.toString() === invalidDateStr) { + return time; + } + switch (timeFormat) { + case \\"locale\\": return validTime.toLocaleTimeString(); + case \\"hours24\\": return validTime.toLocaleTimeString(\\"en-gb\\"); + case \\"hours12\\": return validTime.toLocaleTimeString(\\"en-us\\"); + default: return time; + } +} +export function formatDateTime(dateTimeStr: string, dateTimeFormat: DateTimeFormat[\\"dateTimeFormat\\"]): string { + if (dateTimeStr === undefined || dateTimeStr === null) { + return dateTimeStr; + } + const dateTime = /^d+$/.test(dateTimeStr) ? new Date(Number.parseInt(dateTimeStr, 10)) : new Date(Date.parse(dateTimeStr)); + if (dateTime.toString() === invalidDateStr) { + return dateTimeStr; + } + if (dateTimeFormat === \\"locale\\") { + return dateTime.toLocaleString(); + } + const dateAndTime = dateTime.toISOString().split(\\"T\\"); + const date = formatDate(dateAndTime[0], dateTimeFormat.dateFormat); + const time = formatTime(dateAndTime[1], dateTimeFormat.timeFormat); + return \`\${date} - \${time}\`; +} +export function formatter(value: string, formatterInput: FormatInputType) { + switch (formatterInput.type) { + case \\"DateFormat\\": return formatDate(value, formatterInput.format); + case \\"DateTimeFormat\\": return formatDateTime(value, formatterInput.format); + case \\"TimeFormat\\": return formatTime(value, formatterInput.format); + default: return value; + } +}" +`; diff --git a/packages/codegen-ui-react/lib/__tests__/utils/fetch-by-path.test.ts b/packages/codegen-ui-react/lib/__tests__/utils/fetch-by-path.test.ts new file mode 100644 index 000000000..29a4de2cb --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/utils/fetch-by-path.test.ts @@ -0,0 +1,39 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { fetchByPath } from '../../utils/json-path-fetch'; + +describe('fetch by path util', () => { + const nestedObj = { + levelOne: { + levelTwo: { + levelThree: { + bingo: (value: string) => `Winner Winner ${value}!`, + }, + }, + }, + }; + it('should fetch value from nested object', () => { + const fn: Function = fetchByPath(nestedObj, 'levelOne.levelTwo.levelThree.bingo'); + const result = fn('helloWorld'); + expect(result).toEqual('Winner Winner helloWorld!'); + }); + + it('should return undefined if value does not exist in nested object', () => { + const result = fetchByPath(nestedObj, 'levelOne.levelTwo.nonExistentLevel'); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/utils/string-formatter.test.ts b/packages/codegen-ui-react/lib/__tests__/utils/string-formatter.test.ts new file mode 100644 index 000000000..f6274704f --- /dev/null +++ b/packages/codegen-ui-react/lib/__tests__/utils/string-formatter.test.ts @@ -0,0 +1,23 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { generateFormatUtil } from '../../utils/string-formatter'; +import { assertASTMatchesSnapshot } from '../__utils__'; + +describe('string formatter', () => { + test('generateFormatUtil', () => { + assertASTMatchesSnapshot(generateFormatUtil()); + }); +}); diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts index eb5cfed50..1020bc590 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-form-renderer.ts @@ -13,15 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - StudioNode, - StudioComponent, - StudioComponentChild, - mapFormToComponent, - SchemaModel, - StudioForm, -} from '@aws-amplify/codegen-ui'; +import { StudioNode, StudioComponent, StudioComponentChild } from '@aws-amplify/codegen-ui'; import { JsxElement, JsxFragment, JsxSelfClosingElement } from 'typescript'; + // add primitives in alphabetical order import { AlertProps, @@ -71,35 +65,26 @@ import { VisuallyHiddenProps, TextProps, } from '@aws-amplify/ui-react'; +import { HTMLProps } from 'react'; import { Primitive } from '../primitive'; -import { ReactStudioTemplateRenderer } from '../react-studio-template-renderer'; import CustomComponentRenderer from './customComponent'; import FormRenderer from './form'; import { ReactComponentRenderer } from '../react-component-renderer'; -import { ReactRenderConfig } from '../react-render-config'; - -export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { - protected form: StudioForm; - - constructor(form: StudioForm, modelSchema: SchemaModel, renderConfig: ReactRenderConfig) { - const component = mapFormToComponent(form, modelSchema); - super(component, renderConfig); - this.form = form; - // TODO: update metadata with form definition (either here or in render element) - } +import { ReactFormTemplateRenderer } from '../forms'; +export class AmplifyFormRenderer extends ReactFormTemplateRenderer { renderJsx( - component: StudioComponent | StudioComponentChild, + formComponent: StudioComponent | StudioComponentChild, parent?: StudioNode, ): JsxElement | JsxFragment | JsxSelfClosingElement { - const node = new StudioNode(component, parent); + const node = new StudioNode(formComponent, parent); const renderChildren = (children: StudioComponentChild[]) => children.map((child) => this.renderJsx(child, node)); // add Primitive in alphabetical order - switch (component.componentType) { + switch (formComponent.componentType) { case Primitive.Alert: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -107,7 +92,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Badge: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -115,7 +100,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Button: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -123,7 +108,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ButtonGroup: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -131,7 +116,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Card: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -139,7 +124,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.CheckboxField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -147,8 +132,17 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case 'form': return new FormRenderer( - component, - this.form, + formComponent, + // this component is the current form + this.component, + this.componentMetadata, + this.importCollection, + parent, + ).renderElement(renderChildren); + + case 'option': + return new ReactComponentRenderer>( + formComponent, this.componentMetadata, this.importCollection, parent, @@ -156,7 +150,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Divider: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -164,7 +158,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Expander: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -172,7 +166,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ExpanderItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -180,15 +174,25 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Flex: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, ).renderElement(renderChildren); case Primitive.Grid: + if (!parent) { + return new FormRenderer( + formComponent, + // this component is the current form + this.component, + this.componentMetadata, + this.importCollection, + parent, + ).renderElement(renderChildren); + } return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -196,7 +200,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Heading: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -204,7 +208,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Icon: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -212,7 +216,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Image: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -220,7 +224,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Link: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -228,7 +232,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Loader: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -236,7 +240,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.MenuButton: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -244,7 +248,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.MenuItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -252,7 +256,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Menu: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -260,7 +264,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Pagination: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -268,7 +272,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.PasswordField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -276,7 +280,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.PhoneNumberField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -284,7 +288,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Placeholder: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -292,7 +296,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Radio: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -300,7 +304,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.RadioGroupField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -308,7 +312,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Rating: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -316,7 +320,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ScrollView: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -324,7 +328,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SearchField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -332,7 +336,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SelectField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -340,7 +344,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SliderField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -348,7 +352,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.StepperField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -356,7 +360,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.SwitchField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -364,7 +368,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TabItem: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -372,7 +376,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Tabs: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -380,7 +384,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Table: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -388,7 +392,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableBody: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -396,7 +400,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableCell: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -404,7 +408,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableFoot: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -412,7 +416,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableHead: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -420,7 +424,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TableRow: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -428,7 +432,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.Text: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -436,7 +440,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TextAreaField: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -444,7 +448,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.TextField: return new ReactComponentRenderer>( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -452,7 +456,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ToggleButton: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -460,7 +464,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.ToggleButtonGroup: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -468,7 +472,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.View: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -476,7 +480,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { case Primitive.VisuallyHidden: return new ReactComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, @@ -484,7 +488,7 @@ export class AmplifyFormRenderer extends ReactStudioTemplateRenderer { default: return new CustomComponentRenderer( - component, + formComponent, this.componentMetadata, this.importCollection, parent, diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts new file mode 100644 index 000000000..eed37e3cf --- /dev/null +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts @@ -0,0 +1,36 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { JsxElement, factory, JsxFragment } from 'typescript'; +import { ReactViewTemplateRenderer } from '../views/react-view-renderer'; +import { Primitive } from '../primitive'; +import { ReactTableRenderer } from '../react-table-renderer'; + +export class AmplifyViewRenderer extends ReactViewTemplateRenderer { + renderJsx(): JsxElement | JsxFragment { + switch (this.viewComponent.viewConfiguration.type) { + case Primitive.Table: + return new ReactTableRenderer( + this.viewComponent, + this.viewDefinition, + this.viewMetadata, + this.importCollection, + ).renderElement(); + default: + return factory.createJsxFragment(factory.createJsxOpeningFragment(), [], factory.createJsxJsxClosingFragment()); + } + } +} diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts index c758e2665..1ca4ff86d 100644 --- a/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts +++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/form.ts @@ -21,11 +21,13 @@ import { StudioForm, StudioNode, } from '@aws-amplify/codegen-ui'; -import { factory, JsxAttribute, JsxChild, JsxElement, JsxOpeningElement, SyntaxKind } from 'typescript'; +import { factory, JsxAttribute, JsxChild, JsxElement, JsxOpeningElement, Statement, SyntaxKind } from 'typescript'; import { ReactComponentRenderer } from '../react-component-renderer'; -import { buildOpeningElementProperties } from '../react-component-render-helper'; -import { ImportCollection } from '../imports'; -import { getActionIdentifier } from '../workflow'; +import { buildLayoutProperties, buildOpeningElementProperties } from '../react-component-render-helper'; +import { ImportCollection, ImportSource } from '../imports'; +import { buildDataStoreExpression } from '../forms'; +import { onSubmitValidationRun, buildModelFieldObject } from '../forms/form-renderer-helper'; +import { hasTokenReference } from '../utils/forms/layout-helpers'; export default class FormRenderer extends ReactComponentRenderer { constructor( @@ -48,6 +50,13 @@ export default class FormRenderer extends ReactComponentRenderer { + const fieldTypeToExpressionMap: { + [fieldType: string]: { expression: Expression; identifier: string | BindingName }; + } = { + SliderField: { + expression: expressionMap.e, + identifier: expressionMap.value, + }, + StepperField: { + expression: expressionMap.e, + identifier: expressionMap.value, + }, + SwitchField: { + expression: expressionMap.eTargetChecked, + identifier: expressionMap.value, + }, + CheckboxField: { + expression: expressionMap.eTargetChecked, + identifier: expressionMap.value, + }, + ToggleButton: { + expression: factory.createPrefixUnaryExpression(SyntaxKind.ExclamationToken, factory.createIdentifier(fieldName)), + identifier: expressionMap.value, + }, + }; + + let expression: Expression = fieldTypeToExpressionMap[fieldType]?.expression ?? expressionMap.eTarget; + let defaultIdentifier: string | BindingName = + fieldTypeToExpressionMap[fieldType]?.identifier ?? expressionMap.destructuredValue; + switch (dataType) { + case 'AWSTimestamp': + // value = Number(new Date(e.target.value)); + defaultIdentifier = expressionMap.value; + expression = factory.createCallExpression(factory.createIdentifier('Number'), undefined, [ + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [expressionMap.eTargetValue]), + ]); + break; + case 'Float': + if (fieldType === 'TextField') { + // value = Number(e.target.value); + defaultIdentifier = expressionMap.value; + expression = factory.createCallExpression(factory.createIdentifier('Number'), undefined, [ + expressionMap.eTargetValue, + ]); + } + break; + case 'Int': + if (fieldType === 'TextField') { + // value = parseInt(e.target.value); + defaultIdentifier = expressionMap.value; + expression = factory.createCallExpression(factory.createIdentifier('parseInt'), undefined, [ + expressionMap.eTargetValue, + ]); + } + break; + case 'Boolean': + if (fieldType === 'RadioGroupField') { + // value = e.target.value === 'true' + defaultIdentifier = expressionMap.value; + expression = factory.createBinaryExpression( + expressionMap.eTargetValue, + factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), + factory.createStringLiteral('true'), + ); + } + break; + default: + } + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [factory.createVariableDeclaration(defaultIdentifier, undefined, undefined, expression)], + NodeFlags.Let, + ), + ); +}; diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts new file mode 100644 index 000000000..45dc67769 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts @@ -0,0 +1,1227 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + FieldValidationConfiguration, + FormDefinition, + StudioComponent, + StudioComponentChild, + StudioForm, + FieldConfigMetadata, + FormMetadata, + isControlledComponent, +} from '@aws-amplify/codegen-ui'; +import { + BindingElement, + Expression, + factory, + NodeFlags, + SyntaxKind, + ObjectLiteralElementLike, + ObjectLiteralExpression, + JsxAttribute, + IfStatement, + ExpressionStatement, +} from 'typescript'; +import { lowerCaseFirst } from '../helpers'; +import { ImportCollection, ImportSource } from '../imports'; +import { buildTargetVariable } from './event-targets'; +import { + buildAccessChain, + buildNestedStateSet, + capitalizeFirstLetter, + getCurrentValueIdentifier, + getCurrentValueName, + resetValuesName, + setFieldState, + setStateExpression, +} from './form-state'; + +export const buildMutationBindings = (form: StudioForm) => { + const { + dataType: { dataSourceType, dataTypeName }, + formActionType, + } = form; + const elements: BindingElement[] = []; + if (dataSourceType === 'DataStore') { + if (formActionType === 'update') { + elements.push( + // TODO: change once cpk is supported in datastore + factory.createBindingElement(undefined, undefined, factory.createIdentifier('id'), undefined), + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier(lowerCaseFirst(dataTypeName)), + undefined, + ), + ); + } + elements.push( + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onSuccess'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onError'), undefined), + ); + } + if (dataSourceType === 'Custom' && formActionType === 'update') { + factory.createBindingElement(undefined, undefined, factory.createIdentifier('initialData'), undefined); + } + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('onSubmit'), undefined)); + return elements; +}; + +export const createValidationExpression = (validationRules: FieldValidationConfiguration[] = []): Expression => { + const validateExpressions = validationRules.map((rule) => { + const elements: ObjectLiteralElementLike[] = [ + factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral(rule.type)), + ]; + if ('strValues' in rule) { + elements.push( + factory.createPropertyAssignment( + factory.createIdentifier('strValues'), + factory.createArrayLiteralExpression( + rule.strValues.map((value) => factory.createStringLiteral(value)), + false, + ), + ), + ); + } + if ('numValues' in rule) { + elements.push( + factory.createPropertyAssignment( + factory.createIdentifier('numValues'), + factory.createArrayLiteralExpression( + rule.numValues.map((value) => factory.createNumericLiteral(value)), + false, + ), + ), + ); + } + if (rule.validationMessage) { + elements.push( + factory.createPropertyAssignment( + factory.createIdentifier('validationMessage'), + factory.createStringLiteral(rule.validationMessage), + ), + ); + } + return factory.createObjectLiteralExpression(elements, false); + }); + + return factory.createArrayLiteralExpression(validateExpressions, true); +}; + +export const addFormAttributes = (component: StudioComponent | StudioComponentChild, formMetadata: FormMetadata) => { + const { name: componentName, componentType } = component; + const attributes: JsxAttribute[] = []; + /* + boolean => RadioGroupField + const value = e.target.value.toLowerCase() === 'yes'; + boolean => selectfield + const value = .... + + + componentType => SelectField && boolean + const value = Boolean(e.target.checked) + + */ + + if (componentName in formMetadata.fieldConfigs) { + const fieldConfig = formMetadata.fieldConfigs[componentName]; + /* + if the componetName is a dotPath we need to change the access expression to the following + - bio.user.favorites.Quote => errors['bio.user.favorites.Quote']?.errorMessage + if it's a regular componetName it will use the following expression + - bio => errors.bio?.errorMessage + */ + const errorKey = + componentName.split('.').length > 1 + ? factory.createElementAccessExpression( + factory.createIdentifier('errors'), + factory.createStringLiteral(componentName), + ) + : factory.createPropertyAccessExpression( + factory.createIdentifier('errors'), + factory.createIdentifier(componentName), + ); + attributes.push(...buildComponentSpecificAttributes({ componentType, componentName })); + if (formMetadata.formActionType === 'update' && !fieldConfig.isArray && !isControlledComponent(componentType)) { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('defaultValue'), + factory.createJsxExpression(undefined, factory.createIdentifier(componentName)), + ), + ); + } + attributes.push(buildOnChangeStatement(component, formMetadata.fieldConfigs)); + attributes.push(buildOnBlurStatement(componentName, fieldConfig.isArray)); + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('errorMessage'), + factory.createJsxExpression( + undefined, + factory.createPropertyAccessChain( + errorKey, + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('errorMessage'), + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('hasError'), + factory.createJsxExpression( + undefined, + factory.createPropertyAccessChain( + errorKey, + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + ), + ), + ); + if (fieldConfig.isArray) { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('value'), + factory.createJsxExpression(undefined, getCurrentValueIdentifier(componentName)), + ), + factory.createJsxAttribute( + factory.createIdentifier('ref'), + factory.createJsxExpression(undefined, factory.createIdentifier(`${lowerCaseFirst(componentName)}Ref`)), + ), + ); + } + } + if (componentName === 'ClearButton') { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression(undefined, resetValuesName), + ), + ); + } + if (componentName === 'SubmitButton') { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('isDisabled'), + factory.createJsxExpression( + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('values'), + ), + undefined, + [factory.createIdentifier('errors')], + ), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createPropertyAccessChain( + factory.createIdentifier('e'), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + ), + ], + ), + ), + ), + ); + } + if (componentName === 'CancelButton') { + attributes.push( + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createIdentifier('onCancel'), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createCallExpression(factory.createIdentifier('onCancel'), undefined, []), + ), + ), + ], + false, + ), + ), + ), + ), + ); + } + return attributes; +}; + +/** + if (errors.name?.hasError) { + await runValidationTasks("name", value); + } + */ +function getOnChangeValidationBlock(fieldName: string) { + return factory.createIfStatement( + factory.createPropertyAccessChain( + factory.createPropertyAccessExpression(factory.createIdentifier('errors'), factory.createIdentifier(fieldName)), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression(factory.createIdentifier('runValidationTasks'), undefined, [ + factory.createStringLiteral(fieldName), + factory.createIdentifier('value'), + ]), + ), + ), + ], + true, + ), + undefined, + ); +} + +export function buildOnBlurStatement(fieldName: string, isArray: boolean | undefined) { + return factory.createJsxAttribute( + factory.createIdentifier('onBlur'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression(factory.createIdentifier('runValidationTasks'), undefined, [ + factory.createStringLiteral(fieldName), + isArray ? getCurrentValueIdentifier(fieldName) : factory.createIdentifier(fieldName), + ]), + ), + ), + ); +} + +/** + * if the onChange variable is defined it will send the current state of the fields into the function + * the function expects all fields in return + * the value for that fields onChange will be used from the return object for validation and updating the new state + * + * + * ex. if the field is email + * const returnObject = onChange({ email, ...otherFieldsForForm }); + * const value = returnObject.email; + * + * this value is now used in email validation and setting the state + */ +export const buildOverrideOnChangeStatement = ( + fieldName: string, + fieldConfigs: Record, +): IfStatement => { + const keyPath = fieldName.split('.'); + const keyName = keyPath[0]; + let keyValueExpression = factory.createPropertyAssignment( + factory.createIdentifier(keyName), + factory.createIdentifier('value'), + ); + if (keyPath.length > 1) { + keyValueExpression = factory.createPropertyAssignment( + factory.createIdentifier(keyName), + buildNestedStateSet(keyPath, [keyName], factory.createIdentifier('value')), + ); + } + return factory.createIfStatement( + factory.createIdentifier('onChange'), + factory.createBlock( + [ + buildModelFieldObject(true, fieldConfigs, { + [keyName]: keyValueExpression, + }), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('result'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('onChange'), undefined, [ + factory.createIdentifier('modelFields'), + ]), + ), + ], + NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createIdentifier('value'), + factory.createToken(SyntaxKind.EqualsToken), + factory.createBinaryExpression( + buildAccessChain(['result', ...fieldName.split('.')]), + factory.createToken(SyntaxKind.QuestionQuestionToken), + factory.createIdentifier('value'), + ), + ), + ), + ], + true, + ), + undefined, + ); +}; + +function getOnValueChangeProp(fieldType: string): string { + const map: { [key: string]: string } = { + StepperField: 'onStepChange', + }; + + return map[fieldType] ?? 'onChange'; +} + +export const buildComponentSpecificAttributes = ({ + componentType, + componentName, +}: { + componentType: string; + componentName: string; +}) => { + const stateName = componentName.split('.')[0]; + const componentToAttributesMap: { [key: string]: JsxAttribute[] } = { + ToggleButton: [ + factory.createJsxAttribute( + factory.createIdentifier('isPressed'), + factory.createJsxExpression(undefined, factory.createIdentifier(stateName)), + ), + ], + SliderField: [ + factory.createJsxAttribute( + factory.createIdentifier('value'), + factory.createJsxExpression(undefined, factory.createIdentifier(stateName)), + ), + ], + SelectField: [ + factory.createJsxAttribute( + factory.createIdentifier('value'), + factory.createJsxExpression(undefined, factory.createIdentifier(stateName)), + ), + ], + StepperField: [ + factory.createJsxAttribute( + factory.createIdentifier('value'), + factory.createJsxExpression(undefined, factory.createIdentifier(stateName)), + ), + ], + }; + + return componentToAttributesMap[componentType] ?? []; +}; + +export const buildOnChangeStatement = ( + component: StudioComponent | StudioComponentChild, + fieldConfigs: Record, +) => { + const { name: fieldName, componentType: fieldType } = component; + const { dataType, isArray } = fieldConfigs[fieldName]; + if (isArray) { + return factory.createJsxAttribute( + factory.createIdentifier(getOnValueChangeProp(fieldType)), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + buildTargetVariable(fieldType, fieldName, dataType), + buildOverrideOnChangeStatement(fieldName, fieldConfigs), + getOnChangeValidationBlock(fieldName), + setStateExpression(getCurrentValueName(fieldName), factory.createIdentifier('value')), + ], + true, + ), + ), + ), + ); + } + return factory.createJsxAttribute( + factory.createIdentifier(getOnValueChangeProp(fieldType)), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + buildTargetVariable(fieldType, fieldName, dataType), + buildOverrideOnChangeStatement(fieldName, fieldConfigs), + getOnChangeValidationBlock(fieldName), + factory.createExpressionStatement(setFieldState(fieldName, factory.createIdentifier('value'))), + ], + true, + ), + ), + ), + ); +}; + +export const buildDataStoreExpression = (dataStoreActionType: 'update' | 'create', modelName: string) => { + if (dataStoreActionType === 'update') { + return [ + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('save'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(modelName), + factory.createIdentifier('copyOf'), + ), + undefined, + [ + factory.createIdentifier(`${lowerCaseFirst(modelName)}Record`), + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('updated'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('assign'), + ), + undefined, + [factory.createIdentifier('updated'), factory.createIdentifier('modelFields')], + ), + ), + ], + true, + ), + ), + ], + ), + ], + ), + ), + ), + ]; + } + return [ + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('save'), + ), + undefined, + [ + factory.createNewExpression(factory.createIdentifier(modelName), undefined, [ + factory.createIdentifier('modelFields'), + ]), + ], + ), + ), + ), + ]; +}; + +export const buildOverrideTypesBindings = ( + formComponent: StudioComponent, + formDefinition: FormDefinition, + importCollection: ImportCollection, +) => { + importCollection.addImport(ImportSource.UI_REACT, 'GridProps'); + + const typeNodes = [ + factory.createPropertySignature( + undefined, + factory.createIdentifier(`${formComponent.name}Grid`), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier('FormProps'), [ + factory.createTypeReferenceNode(factory.createIdentifier('GridProps'), undefined), + ]), + ), + ]; + + formDefinition.elementMatrix.forEach((row, index) => { + if (row.length > 1) { + typeNodes.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier(`RowGrid${index}`), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier('FormProps'), [ + factory.createTypeReferenceNode(factory.createIdentifier('GridProps'), undefined), + ]), + ), + ); + } + row.forEach((field) => { + const propKey = + field.split('.').length > 1 ? factory.createStringLiteral(field) : factory.createIdentifier(field); + const componentTypePropName = `${formDefinition.elements[field].componentType}Props`; + typeNodes.push( + factory.createPropertySignature( + undefined, + propKey, + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier('FormProps'), [ + factory.createTypeReferenceNode(factory.createIdentifier(componentTypePropName), undefined), + ]), + ), + ); + importCollection.addImport(ImportSource.UI_REACT, componentTypePropName); + }); + }); + + return factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier(`${formComponent.name}OverridesProps`), + undefined, + factory.createIntersectionTypeNode([ + factory.createTypeLiteralNode(typeNodes), + factory.createTypeReferenceNode(factory.createIdentifier('EscapeHatchProps'), undefined), + ]), + ); +}; + +/** + * builds validation variable + * for nested values it will mention the full path as that corresponds to the fields + * this will also link to error messages + * + * const validations = { post_url: [{ type: "URL" }], 'user.status': [] }; + * + * @param fieldConfigs + * @returns + */ +export function buildValidations(fieldConfigs: Record) { + const validationsForField = Object.entries(fieldConfigs).map(([fieldName, { validationRules }]) => { + const propKey = + fieldName.split('.').length > 1 ? factory.createStringLiteral(fieldName) : factory.createIdentifier(fieldName); + return factory.createPropertyAssignment(propKey, createValidationExpression(validationRules)); + }); + + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validations'), + undefined, + undefined, + factory.createObjectLiteralExpression(validationsForField, true), + ), + ], + NodeFlags.Const, + ), + ); +} + +/** + const runValidationTasks = async (fieldName, value) => { + let validationResponse = validateField(value, validations[fieldName]); + if (onValidate?.[fieldName]) { + validationResponse = await onValidate[fieldName](value, validationResponse); + } + setErrors((errors) => ({ ...errors, [fieldName]: validationResponse })); + return validationResponse; + }; + */ + +export const runValidationTasksFunction = factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('runValidationTasks'), + undefined, + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('fieldName'), + undefined, + undefined, + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validationResponse'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('validateField'), undefined, [ + factory.createIdentifier('value'), + factory.createElementAccessExpression( + factory.createIdentifier('validations'), + factory.createIdentifier('fieldName'), + ), + ]), + ), + ], + NodeFlags.Let, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('customValidator'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('fetchByPath'), undefined, [ + factory.createIdentifier('onValidate'), + factory.createIdentifier('fieldName'), + ]), + ), + ], + NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createIdentifier('customValidator'), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createIdentifier('validationResponse'), + factory.createToken(SyntaxKind.EqualsToken), + factory.createAwaitExpression( + factory.createCallExpression(factory.createIdentifier('customValidator'), undefined, [ + factory.createIdentifier('value'), + factory.createIdentifier('validationResponse'), + ]), + ), + ), + ), + ], + true, + ), + undefined, + ), + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier('setErrors'), undefined, [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('errors'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createParenthesizedExpression( + factory.createObjectLiteralExpression( + [ + factory.createSpreadAssignment(factory.createIdentifier('errors')), + factory.createPropertyAssignment( + factory.createComputedPropertyName(factory.createIdentifier('fieldName')), + factory.createIdentifier('validationResponse'), + ), + ], + false, + ), + ), + ), + ]), + ), + factory.createReturnStatement(factory.createIdentifier('validationResponse')), + ], + true, + ), + ), + ), + ], + NodeFlags.Const, + ), +); +/** + * builds modelFields object which is used to validate, onSubmit, onSuccess/onError + * the nameOverrides will swap in a different expression instead of the name of the state when building the object + * + * ex. [name, content, updatedAt] + * + * const modelFields = { + * name, + * content, + * updatedAt + * }; + * @param fieldConfigs + * @returns + */ +export const buildModelFieldObject = ( + shouldBeConst: boolean, + fieldConfigs: Record = {}, + nameOverrides: Record = {}, +) => { + const fieldSet = new Set(); + const fields = Object.keys(fieldConfigs).reduce((acc, value) => { + const fieldName = value.split('.')[0]; + if (!fieldSet.has(fieldName)) { + const assignment = nameOverrides[fieldName] + ? nameOverrides[fieldName] + : factory.createShorthandPropertyAssignment(factory.createIdentifier(fieldName), undefined); + acc.push(assignment); + fieldSet.add(fieldName); + } + return acc; + }, []); + + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('modelFields'), + undefined, + undefined, + factory.createObjectLiteralExpression(fields, true), + ), + ], + shouldBeConst ? NodeFlags.Const : NodeFlags.Let, + ), + ); +}; + +/** + const validationResponses = await Promise.all( + Object.keys(validations).reduce((promises, fieldName) => { + if (Array.isArray(modelFields[fieldName])) { + promises.push(...modelFields[fieldName].map(item => runValidationTasks(fieldName, item))); + } + promises.push(runValidationTasks(fieldName, modelFields[fieldName])) + return promises + }, []) + ); + + if (validationResponses.some((r) => r.hasError)) { + return; + } + */ + +export const onSubmitValidationRun = [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validationResponses'), + undefined, + undefined, + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Promise'), + factory.createIdentifier('all'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Object'), + factory.createIdentifier('keys'), + ), + undefined, + [factory.createIdentifier('validations')], + ), + factory.createIdentifier('reduce'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('promises'), + undefined, + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('fieldName'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createIfStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Array'), + factory.createIdentifier('isArray'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('modelFields'), + factory.createIdentifier('fieldName'), + ), + ], + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('promises'), + factory.createIdentifier('push'), + ), + undefined, + [ + factory.createSpreadElement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createElementAccessExpression( + factory.createIdentifier('modelFields'), + factory.createIdentifier('fieldName'), + ), + factory.createIdentifier('map'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('item'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createIdentifier('runValidationTasks'), + undefined, + [ + factory.createIdentifier('fieldName'), + factory.createIdentifier('item'), + ], + ), + ), + ], + ), + ), + ], + ), + ), + factory.createReturnStatement(factory.createIdentifier('promises')), + ], + true, + ), + undefined, + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('promises'), + factory.createIdentifier('push'), + ), + undefined, + [ + factory.createCallExpression( + factory.createIdentifier('runValidationTasks'), + undefined, + [ + factory.createIdentifier('fieldName'), + factory.createElementAccessExpression( + factory.createIdentifier('modelFields'), + factory.createIdentifier('fieldName'), + ), + ], + ), + ], + ), + ), + factory.createReturnStatement(factory.createIdentifier('promises')), + ], + true, + ), + ), + factory.createArrayLiteralExpression([], false), + ], + ), + ], + ), + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validationResponses'), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('r'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createPropertyAccessExpression(factory.createIdentifier('r'), factory.createIdentifier('hasError')), + ), + ], + ), + factory.createBlock([factory.createReturnStatement(undefined)], true), + undefined, + ), +]; + +export const ifRecordDefinedExpression = (dataTypeName: string, fieldConfigs: Record) => { + return factory.createIfStatement( + factory.createIdentifier('record'), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier(`set${dataTypeName}Record`), undefined, [ + factory.createIdentifier('record'), + ]), + ), + ...Object.keys(fieldConfigs).map((field) => + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier(`set${capitalizeFirstLetter(field)}`), undefined, [ + factory.createPropertyAccessExpression( + factory.createIdentifier('record'), + factory.createIdentifier(field), + ), + ]), + ), + ), + ], + true, + ), + undefined, + ); +}; + +export const buildSetStateFunction = (fieldConfigs: Record) => { + const fieldSet = new Set(); + const expression = Object.keys(fieldConfigs).reduce((acc, field) => { + const fieldName = field.split('.')[0]; + if (!fieldSet.has(fieldName)) { + acc.push( + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier(`set${capitalizeFirstLetter(fieldName)}`), undefined, [ + factory.createPropertyAccessExpression( + factory.createIdentifier('initialData'), + factory.createIdentifier(fieldName), + ), + ]), + ), + ); + fieldSet.add(fieldName); + } + return acc; + }, []); + return factory.createIfStatement(factory.createIdentifier('initialData'), factory.createBlock(expression, true)); +}; + +export const buildUpdateDatastoreQuery = (dataTypeName: string, fieldConfigs: Record) => { + // TODO: update this once cpk is supported in datastore + const pkQueryIdentifier = factory.createIdentifier('id'); + return [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('queryData'), + undefined, + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('record'), + undefined, + undefined, + factory.createConditionalExpression( + pkQueryIdentifier, + factory.createToken(SyntaxKind.QuestionToken), + factory.createAwaitExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('DataStore'), + factory.createIdentifier('query'), + ), + undefined, + [factory.createIdentifier(dataTypeName), pkQueryIdentifier], + ), + ), + factory.createToken(SyntaxKind.ColonToken), + factory.createIdentifier(lowerCaseFirst(dataTypeName)), + ), + ), + ], + NodeFlags.Const, + ), + ), + ifRecordDefinedExpression(dataTypeName, fieldConfigs), + ], + true, + ), + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier('queryData'), undefined, []), + ), + ]; +}; diff --git a/packages/codegen-ui-react/lib/forms/form-state.ts b/packages/codegen-ui-react/lib/forms/form-state.ts new file mode 100644 index 000000000..67383068a --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/form-state.ts @@ -0,0 +1,255 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { FieldConfigMetadata, DataFieldDataType } from '@aws-amplify/codegen-ui'; +import { + factory, + Statement, + Expression, + NodeFlags, + Identifier, + SyntaxKind, + ObjectLiteralExpression, + CallExpression, +} from 'typescript'; + +export const getCurrentValueName = (fieldName: string) => `current${capitalizeFirstLetter(fieldName)}Value`; + +export const getCurrentValueIdentifier = (fieldName: string) => + factory.createIdentifier(getCurrentValueName(fieldName)); + +export const resetValuesName = factory.createIdentifier('resetStateValues'); + +export const setStateExpression = (fieldName: string, value: Expression) => { + return factory.createExpressionStatement( + factory.createCallExpression(getSetNameIdentifier(fieldName), undefined, [value]), + ); +}; + +export const capitalizeFirstLetter = (val: string) => { + return val.charAt(0).toUpperCase() + val.slice(1); +}; + +export const getSetNameIdentifier = (value: string): Identifier => { + return factory.createIdentifier(`set${capitalizeFirstLetter(value)}`); +}; + +export const getDefaultValueExpression = ( + name: string, + componentType: string, + dataType?: DataFieldDataType, +): Expression => { + const componentTypeToDefaultValueMap: { [key: string]: Expression } = { + ToggleButton: factory.createFalse(), + StepperField: factory.createNumericLiteral(0), + SliderField: factory.createNumericLiteral(0), + }; + + // it's a nonModel or relationship object + if (dataType && typeof dataType === 'object' && !('enum' in dataType)) { + return factory.createObjectLiteralExpression(); + } + // the name itself is a nested json object + if (name.split('.').length > 1) { + return factory.createObjectLiteralExpression(); + } + + if (componentType in componentTypeToDefaultValueMap) { + return componentTypeToDefaultValueMap[componentType]; + } + return factory.createIdentifier('undefined'); +}; + +/** + * iterates field configs to create useState hooks for each field + * populates the default values as undefined if it as a nested object, relationship model or nonModel + * the default is an empty object + * @param fieldConfigs + * @returns + */ +export const getUseStateHooks = (fieldConfigs: Record): Statement[] => { + const stateNames = new Set(); + return Object.entries(fieldConfigs).reduce((acc, [name, { dataType, componentType }]) => { + const stateName = name.split('.')[0]; + if (!stateNames.has(stateName)) { + acc.push(buildUseStateExpression(stateName, getDefaultValueExpression(name, componentType, dataType))); + stateNames.add(stateName); + } + return acc; + }, []); +}; + +/** + * function used by the onClear/onReset button cta + * it's a reset type but we also need to clear the state of the input fields as well + * + * ex. + * const resetStateValues = () => { + * setName('') + * setLastName('') + * .... + * }; + */ +export const resetStateFunction = (fieldConfigs: Record) => { + const stateNames = new Set(); + const setStateExpressions = Object.entries(fieldConfigs).reduce( + (acc, [name, { dataType, componentType, isArray }]) => { + const stateName = name.split('.')[0]; + if (!stateNames.has(stateName)) { + acc.push(setStateExpression(stateName, getDefaultValueExpression(name, componentType, dataType))); + if (isArray) { + acc.push( + setStateExpression( + getCurrentValueName(stateName), + getDefaultValueExpression(name, componentType, dataType), + ), + ); + } + stateNames.add(stateName); + } + return acc; + }, + [], + ); + // also reset the state of the errors + setStateExpressions.push(setStateExpression('errors', factory.createObjectLiteralExpression())); + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + resetValuesName, + undefined, + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock(setStateExpressions, true), + ), + ), + ], + NodeFlags.Const, + ), + ); +}; + +/** + * const [name, setName] = React.useState({default_expression}); + * + * name is the value we are looking to set + * defaultValue is is the value to set for the useState + * @param name + * @param defaultValue + * @returns + */ +export const buildUseStateExpression = (name: string, defaultValue: Expression): Statement => { + return factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createArrayBindingPattern([ + factory.createBindingElement(undefined, undefined, factory.createIdentifier(name), undefined), + factory.createBindingElement(undefined, undefined, getSetNameIdentifier(name), undefined), + ]), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('React'), + factory.createIdentifier('useState'), + ), + undefined, + [defaultValue], + ), + ), + ], + NodeFlags.Const, + ), + ); +}; + +/** + * turns ['myNestedObject', 'value', 'nestedValue', 'leaf'] + * + * into myNestedObject?.value?.nestedValue?.leaf + * + * @param values + * @returns + */ +export const buildAccessChain = (values: string[], isOptional = true): Expression => { + if (values.length <= 0) { + throw new Error('Need at least one value in the values array'); + } + const optional = isOptional ? factory.createToken(SyntaxKind.QuestionDotToken) : undefined; + if (values.length > 1) { + const [parent, child, ...rest] = values; + let propChain = factory.createPropertyAccessChain( + factory.createIdentifier(parent), + optional, + factory.createIdentifier(child), + ); + if (rest.length) { + rest.forEach((value) => { + propChain = factory.createPropertyAccessChain(propChain, optional, factory.createIdentifier(value)); + }); + } + return propChain; + } + return factory.createIdentifier(values[0]); +}; + +export const buildNestedStateSet = ( + keyPath: string[], + currentKeyPath: string[], + value: Expression, + index = 1, +): ObjectLiteralExpression => { + if (keyPath.length <= 1) { + throw new Error('keyPath needs a length larger than 1 to build nested state object'); + } + const currentKey = keyPath[index]; + // the value of the index is what decides if we have reached the leaf property of the nested object + if (keyPath.length - 1 === index) { + return factory.createObjectLiteralExpression([ + factory.createSpreadAssignment(buildAccessChain(currentKeyPath)), + factory.createPropertyAssignment(factory.createIdentifier(currentKey), value), + ]); + } + const currentSpreadAssignment = buildAccessChain(currentKeyPath); + currentKeyPath.push(currentKey); + return factory.createObjectLiteralExpression([ + factory.createSpreadAssignment(currentSpreadAssignment), + factory.createPropertyAssignment( + factory.createIdentifier(currentKey), + buildNestedStateSet(keyPath, currentKeyPath, value, index + 1), + ), + ]); +}; + +// updating state +export const setFieldState = (name: string, value: Expression): CallExpression => { + if (name.split('.').length > 1) { + const keyPath = name.split('.'); + return factory.createCallExpression(getSetNameIdentifier(keyPath[0]), undefined, [ + buildNestedStateSet(keyPath, [keyPath[0]], value), + ]); + } + return factory.createCallExpression(getSetNameIdentifier(name), undefined, [value]); +}; diff --git a/packages/codegen-ui-react/lib/forms/index.ts b/packages/codegen-ui-react/lib/forms/index.ts new file mode 100644 index 000000000..3f65f6644 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/index.ts @@ -0,0 +1,18 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export * from './form-renderer-helper'; +export * from './react-form-renderer'; +export * from './type-helper'; diff --git a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts new file mode 100644 index 000000000..b989fd728 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -0,0 +1,463 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentMetadata, + computeComponentMetadata, + FormDefinition, + generateFormDefinition, + GenericDataSchema, + handleCodegenErrors, + mapFormDefinitionToComponent, + mapFormMetadata, + StudioComponent, + StudioForm, + StudioNode, + StudioTemplateRenderer, + validateFormSchema, +} from '@aws-amplify/codegen-ui'; +import { EOL } from 'os'; +import { + addSyntheticLeadingComment, + BindingElement, + EmitHint, + factory, + FunctionDeclaration, + JsxElement, + JsxFragment, + JsxSelfClosingElement, + Modifier, + NodeFlags, + ScriptKind, + Statement, + SyntaxKind, + TypeAliasDeclaration, +} from 'typescript'; +import { lowerCaseFirst } from '../helpers'; +import { ImportCollection, ImportSource, ImportValue } from '../imports'; +import { PrimitiveTypeParameter, Primitive } from '../primitive'; +import { getComponentPropName } from '../react-component-render-helper'; +import { ReactOutputManager } from '../react-output-manager'; +import { ReactRenderConfig, scriptKindToFileExtension } from '../react-render-config'; +import { + buildPrinter, + defaultRenderConfig, + getDeclarationFilename, + transpile, +} from '../react-studio-template-renderer-helper'; +import { generateArrayFieldComponent } from '../utils/forms/array-field-component'; +import { hasTokenReference } from '../utils/forms/layout-helpers'; +import { addUseEffectWrapper } from '../utils/generate-react-hooks'; +import { RequiredKeys } from '../utils/type-utils'; +import { + buildMutationBindings, + buildOverrideTypesBindings, + buildSetStateFunction, + buildUpdateDatastoreQuery, + buildValidations, + runValidationTasksFunction, +} from './form-renderer-helper'; +import { buildUseStateExpression, getCurrentValueName, getUseStateHooks, resetStateFunction } from './form-state'; +import { + buildFormPropNode, + baseValidationConditionalType, + formOverrideProp, + generateInputTypes, + validationFunctionType, + validationResponseType, +} from './type-helper'; + +export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< + string, + StudioForm, + ReactOutputManager, + { + componentText: string; + renderComponentToFilesystem: (outputPath: string) => Promise; + } +> { + protected importCollection = new ImportCollection(); + + protected renderConfig: RequiredKeys; + + protected formDefinition: FormDefinition; + + protected formComponent: StudioComponent; + + protected componentMetadata: ComponentMetadata; + + public fileName: string; + + constructor(component: StudioForm, dataSchema: GenericDataSchema | undefined, renderConfig: ReactRenderConfig) { + super(component, new ReactOutputManager(), renderConfig); + this.renderConfig = { + ...defaultRenderConfig, + ...renderConfig, + }; + // the super class creates a component aka form which is what we pass in this extended implmentation + this.fileName = `${this.component.name}.${scriptKindToFileExtension(this.renderConfig.script)}`; + + this.formDefinition = generateFormDefinition({ form: component, dataSchema }); + + // create a studio component which will represent the structure of the form + this.formComponent = mapFormDefinitionToComponent(this.component.name, this.formDefinition); + + this.componentMetadata = computeComponentMetadata(this.formComponent); + this.componentMetadata.formMetadata = mapFormMetadata(this.component, this.formDefinition, dataSchema); + } + + @handleCodegenErrors + renderComponentOnly() { + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.formComponent); + const requiredDataModels = []; + + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + const imports = this.importCollection.buildImportStatements(); + + let importsText = ''; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + importsText += result + EOL; + }); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, false); + let result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + + if (this.componentMetadata.formMetadata) { + if (Object.values(this.componentMetadata.formMetadata?.fieldConfigs).some(({ isArray }) => isArray)) { + const arrayFieldText = printer.printNode(EmitHint.Unspecified, generateArrayFieldComponent(), file); + result = arrayFieldText + EOL + result; + } + } + // do not produce declaration becuase it is not used + const { componentText: compText } = transpile(result, { ...this.renderConfig, renderTypeDeclarations: false }); + + if (this.component.dataType.dataSourceType === 'DataStore') { + requiredDataModels.push(this.component.dataType.dataTypeName); + // TODO: require other models if form is handling querying relational models + } + + return { compText, importsText, requiredDataModels }; + } + + renderComponentInternal() { + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + // build form related variable statments + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.formComponent); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, true); + const propsDeclaration = this.renderBindingPropsType(); + + const imports = this.importCollection.buildImportStatements(); + + let componentText = `/* eslint-disable */${EOL}`; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + componentText += result + EOL; + }); + + componentText += EOL; + + propsDeclaration.forEach((typeNode) => { + const propsPrinted = printer.printNode(EmitHint.Unspecified, typeNode, file); + componentText += propsPrinted; + }); + + if (this.componentMetadata.formMetadata) { + if (Object.values(this.componentMetadata.formMetadata?.fieldConfigs).some(({ isArray }) => isArray)) { + const arrayFieldComponent = printer.printNode(EmitHint.Unspecified, generateArrayFieldComponent(), file); + componentText += arrayFieldComponent; + } + } + + const result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + componentText += result; + const { componentText: transpiledComponentText, declaration } = transpile(componentText, this.renderConfig); + + return { + componentText: transpiledComponentText, + declaration, + formMetadata: this.componentMetadata.formMetadata, + renderComponentToFilesystem: async (outputPath: string) => { + await this.renderComponentToFilesystem(transpiledComponentText)(this.fileName)(outputPath); + if (declaration) { + await this.renderComponentToFilesystem(declaration)(getDeclarationFilename(this.fileName))(outputPath); + } + }, + }; + } + + renderFunctionWrapper( + componentName: string, + variableStatements: Statement[], + jsx: JsxElement | JsxFragment | JsxSelfClosingElement, + renderExport: boolean, + ): FunctionDeclaration { + const componentPropType = getComponentPropName(componentName); + const jsxStatement = factory.createReturnStatement( + factory.createParenthesizedExpression( + this.renderConfig.script !== ScriptKind.TSX + ? jsx + : /* add ts-ignore comment above jsx statement. Generated props are incompatible with amplify-ui props */ + addSyntheticLeadingComment( + factory.createParenthesizedExpression(jsx), + SyntaxKind.MultiLineCommentTrivia, + ' @ts-ignore: TS2322 ', + true, + ), + ), + ); + const codeBlockContent = variableStatements.concat([jsxStatement]); + const modifiers: Modifier[] = renderExport + ? [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DefaultKeyword)] + : []; + const typeParameter = PrimitiveTypeParameter[Primitive[this.formComponent?.componentType as Primitive]]; + // only use type parameter reference if one was declared + const typeParameterReference = typeParameter && typeParameter.declaration() ? typeParameter.reference() : undefined; + return factory.createFunctionDeclaration( + undefined, + modifiers, + undefined, + factory.createIdentifier(componentName), + typeParameter ? typeParameter.declaration() : undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'props', + undefined, + factory.createTypeReferenceNode(componentPropType, typeParameterReference), + undefined, + ), + ], + factory.createTypeReferenceNode( + factory.createQualifiedName(factory.createIdentifier('React'), factory.createIdentifier('ReactElement')), + undefined, + ), + factory.createBlock(codeBlockContent, true), + ); + } + + abstract renderJsx(component: StudioComponent, parent?: StudioNode): JsxElement | JsxFragment | JsxSelfClosingElement; + + private renderBindingPropsType(): TypeAliasDeclaration[] { + const { + name: formName, + formActionType, + dataType: { dataSourceType, dataTypeName }, + } = this.component; + const fieldConfigs = this.componentMetadata.formMetadata?.fieldConfigs ?? {}; + const overrideTypeAliasDeclaration = buildOverrideTypesBindings( + this.formComponent, + this.formDefinition, + this.importCollection, + ); + const escapeHatchTypeNode = factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('overrides'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(`${formName}OverridesProps`, undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + factory.createLiteralTypeNode(factory.createNull()), + ]), + ), + ]); + const formPropType = getComponentPropName(formName); + + this.importCollection.addMappedImport(ImportValue.ESCAPE_HATCH_PROPS); + if (dataSourceType === 'DataStore' && formActionType === 'update') { + this.importCollection.addImport(ImportSource.LOCAL_MODELS, dataTypeName); + } + + return [ + validationResponseType, + validationFunctionType, + baseValidationConditionalType, + generateInputTypes(formName, fieldConfigs), + formOverrideProp, + overrideTypeAliasDeclaration, + factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier(formPropType), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('React.PropsWithChildren'), [ + factory.createIntersectionTypeNode([escapeHatchTypeNode, buildFormPropNode(this.component)]), + ]), + ), + ]; + } + + /** + * Variable Statements need for forms + * - props passed into form component + * - useState + * - form fields + * - valid state for form + * - error object { hasError: boolean, errorMessage: string } + * - datastore operation (conditional if form is backed by datastore) + * - this is the datastore mutation function which will be used by the helpers + */ + private buildVariableStatements() { + const statements: Statement[] = []; + const { formMetadata } = this.componentMetadata; + const { + dataType: { dataTypeName, dataSourceType }, + formActionType, + } = this.component; + const lowerCaseDataTypeName = lowerCaseFirst(dataTypeName); + + if (!formMetadata) { + throw new Error(`Form Metadata is missing from form: ${this.component.name}`); + } + + const elements: BindingElement[] = [ + // add in hooks for before/complete with ds and basic onSubmit with props + ...buildMutationBindings(this.component), + // onCancel prop + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onCancel'), undefined), + // onValidate prop + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onValidate'), undefined), + // onChange prop + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onChange'), undefined), + // overrides + factory.createBindingElement(undefined, undefined, factory.createIdentifier('overrides'), undefined), + // get rest of props to pass to top level component + factory.createBindingElement( + factory.createToken(SyntaxKind.DotDotDotToken), + undefined, + factory.createIdentifier('rest'), + undefined, + ), + ]; + + // add binding elments to statements + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createObjectBindingPattern(elements), + undefined, + undefined, + factory.createIdentifier('props'), + ), + ], + NodeFlags.Const, + ), + ), + ); + + if (hasTokenReference(this.componentMetadata)) { + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createObjectBindingPattern([ + factory.createBindingElement(undefined, undefined, factory.createIdentifier('tokens'), undefined), + ]), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('useTheme'), undefined, []), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + + statements.push(...getUseStateHooks(formMetadata.fieldConfigs)); + + statements.push(buildUseStateExpression('errors', factory.createObjectLiteralExpression())); + + statements.push(resetStateFunction(formMetadata.fieldConfigs)); + + this.importCollection.addMappedImport(ImportValue.VALIDATE_FIELD); + this.importCollection.addMappedImport(ImportValue.FETCH_BY_PATH); + + // add model import for datastore type + if (dataSourceType === 'DataStore') { + this.importCollection.addImport(ImportSource.LOCAL_MODELS, dataTypeName); + if (formActionType === 'update') { + statements.push( + buildUseStateExpression(`${lowerCaseDataTypeName}Record`, factory.createIdentifier(lowerCaseDataTypeName)), + ); + statements.push( + addUseEffectWrapper( + buildUpdateDatastoreQuery(dataTypeName, formMetadata.fieldConfigs), + // TODO: change once cpk is supported in datastore + ['id', lowerCaseDataTypeName], + ), + ); + } + } + if (dataSourceType === 'Custom' && formActionType === 'update') { + statements.push(addUseEffectWrapper([buildSetStateFunction(formMetadata.fieldConfigs)], [])); + } + + this.importCollection.addMappedImport(ImportValue.VALIDATE_FIELD); + // Add value state and ref array type fields in ArrayField wrapper + Object.entries(formMetadata.fieldConfigs).forEach(([field, config]) => { + if (config.isArray) { + statements.push( + buildUseStateExpression(getCurrentValueName(field), factory.createStringLiteral('')), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(`${field}Ref`), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('React'), + factory.createIdentifier('createRef'), + ), + undefined, + [], + ), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + }); + statements.push(buildValidations(formMetadata.fieldConfigs)); + statements.push(runValidationTasksFunction); + + return statements; + } + + protected validateSchema(component: StudioForm): void { + validateFormSchema(component); + } +} diff --git a/packages/codegen-ui-react/lib/forms/type-helper.ts b/packages/codegen-ui-react/lib/forms/type-helper.ts new file mode 100644 index 000000000..58be7ac56 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/type-helper.ts @@ -0,0 +1,452 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { FieldConfigMetadata, DataFieldDataType, StudioForm } from '@aws-amplify/codegen-ui'; +import { factory, SyntaxKind, KeywordTypeSyntaxKind, TypeElement, PropertySignature, TypeNode } from 'typescript'; +import { lowerCaseFirst } from '../helpers'; +import { DATA_TYPE_TO_TYPESCRIPT_MAP, FIELD_TYPE_TO_TYPESCRIPT_MAP } from './typescript-type-map'; + +type Node = { + [n: string]: T | Node; +}; + +type GetTypeNodeParam = { componentType: string; dataType?: DataFieldDataType }; +/** + * based on the provided dataType (appsync scalar) + * converst to the correct typescript type + * default assumption is string type + */ +const getTypeNode = ({ componentType, dataType }: GetTypeNodeParam) => { + let typescriptType: KeywordTypeSyntaxKind = SyntaxKind.StringKeyword; + if (componentType in FIELD_TYPE_TO_TYPESCRIPT_MAP) { + typescriptType = FIELD_TYPE_TO_TYPESCRIPT_MAP[componentType]; + } + + if (dataType && typeof dataType === 'string' && dataType in DATA_TYPE_TO_TYPESCRIPT_MAP) { + typescriptType = DATA_TYPE_TO_TYPESCRIPT_MAP[dataType]; + } + + // e.g. string + return factory.createKeywordTypeNode(typescriptType); +}; + +export const getInputValuesTypeName = (formName: string): string => { + return `${formName}InputValues`; +}; + +/** + * given the nested json paths rejoin them into one object + * where the leafs are the types ex. string | number | boolean + * src: https://stackoverflow.com/questions/70218560/creating-a-nested-object-from-entries + * + * @param nestedPaths + */ +export const generateObjectFromPaths = ( + object: Node, + [key, value]: [fieldName: string, getTypeNodeParam: GetTypeNodeParam], +) => { + const keys = key.split('.'); + const last = keys.pop() ?? ''; + // eslint-disable-next-line no-return-assign, no-param-reassign + keys.reduce((o: any, k: string) => (o[k] ??= {}), object)[last] = getTypeNode(value); + return object; +}; + +export const generateTypeNodeFromObject = (obj: Node): PropertySignature[] => { + return Object.keys(obj).map((key) => { + const child = obj[key]; + const value: TypeNode = + typeof child === 'object' && Object.getPrototypeOf(child) === Object.prototype + ? factory.createTypeLiteralNode(generateTypeNodeFromObject(child)) + : factory.createTypeReferenceNode(factory.createIdentifier('UseBaseOrValidationType'), [ + factory.createTypeReferenceNode(factory.createIdentifier('useBase'), undefined), + child as unknown as TypeNode, + ]); + return factory.createPropertySignature( + undefined, + factory.createIdentifier(key), + factory.createToken(SyntaxKind.QuestionToken), + value, + ); + }); +}; + +/** + * this generates the input types for onSubmit, onSuccess, onChange, and onValidate + * onValidate is the one case where it passes true to get the ValidationType + * instead of the base type + * + * export declare type MyPostCreateFormInputValues = { + caption: UseBaseOrValidationType; + phoneNumber: UseBaseOrValidationType; + username: UseBaseOrValidationType; + post_url: UseBaseOrValidationType; + profile_url: UseBaseOrValidationType; + status: UseBaseOrValidationType; + bio: { + favoriteQuote: UseBaseOrValidationType; + favoiteAnimal: { + genus: UseBaseOrValidationType; + } + }; + * }; + * + * + * @param formName + * @param fieldConfigs + * @returns + */ +export const generateInputTypes = (formName: string, fieldConfigs: Record) => { + const nestedPaths: [fieldName: string, getTypeNodeParam: GetTypeNodeParam][] = []; + const typeNodes: TypeElement[] = []; + Object.entries(fieldConfigs).forEach(([fieldName, { dataType, componentType }]) => { + const getTypeNodeParam = { dataType, componentType }; + const hasNestedFieldPath = fieldName.split('.').length > 1; + if (hasNestedFieldPath) { + nestedPaths.push([fieldName, getTypeNodeParam]); + } else { + typeNodes.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier(fieldName), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier('UseBaseOrValidationType'), [ + factory.createTypeReferenceNode(factory.createIdentifier('useBase'), undefined), + getTypeNode(getTypeNodeParam), + ]), + ), + ); + } + }); + + if (nestedPaths.length) { + const nestedObj = nestedPaths.reduce(generateObjectFromPaths, {}); + const nestedTypeNodes = generateTypeNodeFromObject(nestedObj); + typeNodes.push(...nestedTypeNodes); + } + return factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier(getInputValuesTypeName(formName)), + [ + factory.createTypeParameterDeclaration( + factory.createIdentifier('useBase'), + factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword), + factory.createLiteralTypeNode(factory.createTrue()), + ), + ], + factory.createTypeLiteralNode(typeNodes), + ); +}; + +/** + * used to validate if value should be using the base type or ValdiationFunction using base type + * + * export declare type UseBaseOrValidationType = Flag extends true ? T : ValidationFunction; + */ +export const baseValidationConditionalType = factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier('UseBaseOrValidationType'), + [ + factory.createTypeParameterDeclaration(factory.createIdentifier('Flag'), undefined, undefined), + factory.createTypeParameterDeclaration(factory.createIdentifier('T'), undefined, undefined), + ], + factory.createConditionalTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('Flag'), undefined), + factory.createLiteralTypeNode(factory.createTrue()), + factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined), + factory.createTypeReferenceNode(factory.createIdentifier('ValidationFunction'), [ + factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined), + ]), + ), +); + +/** + * export declare type ValidationResponse = { + * hasError: boolean; + * errorMessage?: string; + * }; + */ +export const validationResponseType = factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier('ValidationResponse'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('hasError'), + undefined, + factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('errorMessage'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ), + ]), +); + +/** + * export declare type ValidationResponse = { + * hasError: boolean; + * errorMessage?: string; + * }; + */ +export const formOverrideProp = factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier('FormProps'), + [factory.createTypeParameterDeclaration(factory.createIdentifier('T'), undefined, undefined)], + factory.createIntersectionTypeNode([ + factory.createTypeReferenceNode(factory.createIdentifier('Partial'), [ + factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined), + ]), + factory.createTypeReferenceNode( + factory.createQualifiedName(factory.createIdentifier('React'), factory.createIdentifier('DOMAttributes')), + [factory.createTypeReferenceNode(factory.createIdentifier('HTMLDivElement'), undefined)], + ), + ]), +); + +/** + * onValidate?: {formTypeName} + * + * @param formName + * @returns + */ +export const buildOnValidateType = (formName: string) => { + return factory.createPropertySignature( + undefined, + factory.createIdentifier('onValidate'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), [ + factory.createLiteralTypeNode(factory.createFalse()), + ]), + ); +}; + +/** + * export declare type ValidationFunction = + * (value: T, validationResponse: ValidationResponse) => ValidationResponse | Promise; + */ +export const validationFunctionType = factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DeclareKeyword)], + factory.createIdentifier('ValidationFunction'), + [factory.createTypeParameterDeclaration(factory.createIdentifier('T'), undefined, undefined)], + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('validationResponse'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('ValidationResponse'), undefined), + undefined, + ), + ], + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(factory.createIdentifier('ValidationResponse'), undefined), + factory.createTypeReferenceNode(factory.createIdentifier('Promise'), [ + factory.createTypeReferenceNode(factory.createIdentifier('ValidationResponse'), undefined), + ]), + ]), + ), +); + +/* + both datastore & custom datasource has onSubmit with the fields + - onSubmit(fields) + datastore includes additional hooks + - onSuccess(fields) + - onError(fields, errorMessage) + */ +export const buildFormPropNode = (form: StudioForm) => { + const { + name: formName, + dataType: { dataSourceType, dataTypeName }, + formActionType, + } = form; + const propSignatures: PropertySignature[] = []; + if (dataSourceType === 'DataStore') { + if (formActionType === 'update') { + propSignatures.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier('id'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier(lowerCaseFirst(dataTypeName)), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(factory.createIdentifier(dataTypeName), undefined), + ), + ); + } + propSignatures.push( + factory.createPropertySignature( + undefined, + 'onSubmit', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'fields', + undefined, + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + undefined, + ), + ], + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + ), + ), + factory.createPropertySignature( + undefined, + 'onSuccess', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('fields'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + undefined, + ), + ], + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('onError'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('fields'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('errorMessage'), + undefined, + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + undefined, + ), + ], + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ), + ), + ); + } + if (dataSourceType === 'Custom') { + if (formActionType === 'update') { + propSignatures.push( + factory.createPropertySignature( + undefined, + factory.createIdentifier('initialData'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createTypeReferenceNode(getInputValuesTypeName(formName), undefined), + ), + ); + } + propSignatures.push( + factory.createPropertySignature( + undefined, + 'onSubmit', + undefined, + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'fields', + undefined, + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + undefined, + ), + ], + factory.createKeywordTypeNode(SyntaxKind.VoidKeyword), + ), + ), + ); + } + propSignatures.push( + // onCancel?: () => void + factory.createPropertySignature( + undefined, + 'onCancel', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode(undefined, [], factory.createKeywordTypeNode(SyntaxKind.VoidKeyword)), + ), + // onChange?: (fields: Record) => Record + factory.createPropertySignature( + undefined, + 'onChange', + factory.createToken(SyntaxKind.QuestionToken), + factory.createFunctionTypeNode( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('fields'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + undefined, + ), + ], + factory.createTypeReferenceNode(factory.createIdentifier(getInputValuesTypeName(formName)), undefined), + ), + ), + buildOnValidateType(form.name), + ); + return factory.createTypeLiteralNode(propSignatures); +}; diff --git a/packages/codegen-ui-react/lib/forms/typescript-type-map.ts b/packages/codegen-ui-react/lib/forms/typescript-type-map.ts new file mode 100644 index 000000000..6b8b1d3c7 --- /dev/null +++ b/packages/codegen-ui-react/lib/forms/typescript-type-map.ts @@ -0,0 +1,31 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { KeywordTypeSyntaxKind, SyntaxKind } from 'typescript'; + +export const DATA_TYPE_TO_TYPESCRIPT_MAP: { [key: string]: KeywordTypeSyntaxKind } = { + Int: SyntaxKind.NumberKeyword, + Float: SyntaxKind.NumberKeyword, + Boolean: SyntaxKind.BooleanKeyword, + AWSTimestamp: SyntaxKind.NumberKeyword, +}; + +export const FIELD_TYPE_TO_TYPESCRIPT_MAP: { [key: string]: KeywordTypeSyntaxKind } = { + SliderField: SyntaxKind.NumberKeyword, + StepperField: SyntaxKind.NumberKeyword, + SwitchField: SyntaxKind.BooleanKeyword, + CheckboxField: SyntaxKind.BooleanKeyword, + ToggleButton: SyntaxKind.BooleanKeyword, +}; diff --git a/packages/codegen-ui-react/lib/helpers/index.ts b/packages/codegen-ui-react/lib/helpers/index.ts new file mode 100644 index 000000000..6d9e1095d --- /dev/null +++ b/packages/codegen-ui-react/lib/helpers/index.ts @@ -0,0 +1,16 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export const lowerCaseFirst = (input: string) => input.charAt(0).toLowerCase() + input.slice(1); diff --git a/packages/codegen-ui-react/lib/imports/import-collection.ts b/packages/codegen-ui-react/lib/imports/import-collection.ts index a94065d9b..c9a9ca098 100644 --- a/packages/codegen-ui-react/lib/imports/import-collection.ts +++ b/packages/codegen-ui-react/lib/imports/import-collection.ts @@ -13,9 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import factory, { ImportDeclaration } from 'typescript'; +import { ImportDeclaration, factory } from 'typescript'; import path from 'path'; -import { ImportMapping, ImportValue } from './import-mapping'; +import { ImportMapping, ImportValue, ImportSource } from './import-mapping'; export class ImportCollection { #collection: Map> = new Map(); @@ -37,6 +37,10 @@ export class ImportCollection { } } + removeImportSource(packageImport: ImportSource) { + this.#collection.delete(packageImport); + } + mergeCollections(otherCollection: ImportCollection) { otherCollection.#collection.forEach((value, key) => { [...value].forEach((singlePackage) => { @@ -51,6 +55,7 @@ export class ImportCollection { undefined, undefined, factory.createImportClause( + false, undefined, factory.createNamedImports([ factory.createImportSpecifier(undefined, factory.createIdentifier(topComponentName)), @@ -70,7 +75,11 @@ export class ImportCollection { factory.createImportDeclaration( undefined, undefined, - factory.createImportClause(factory.createIdentifier('React'), undefined), + factory.createImportClause( + false, + undefined, + factory.createNamespaceImport(factory.createIdentifier('React')), + ), factory.createStringLiteral('react'), ), ], @@ -82,6 +91,7 @@ export class ImportCollection { undefined, undefined, factory.createImportClause( + false, // use module name as defualt import name [...imports].indexOf('default') >= 0 ? factory.createIdentifier(path.basename(moduleName)) : undefined, factory.createNamedImports( diff --git a/packages/codegen-ui-react/lib/imports/import-mapping.ts b/packages/codegen-ui-react/lib/imports/import-mapping.ts index 351bfe6b1..6e6053ee2 100644 --- a/packages/codegen-ui-react/lib/imports/import-mapping.ts +++ b/packages/codegen-ui-react/lib/imports/import-mapping.ts @@ -20,6 +20,7 @@ export enum ImportSource { AMPLIFY_DATASTORE = '@aws-amplify/datastore', LOCAL_MODELS = '../models', LOCAL_SCHEMA = '../models/schema', + UTILS = './utils', } export enum ImportValue { @@ -42,6 +43,10 @@ export enum ImportValue { USE_AUTH_SIGN_OUT_ACTION = 'useAuthSignOutAction', USE_STATE_MUTATION_ACTION = 'useStateMutationAction', USE_EFFECT = 'useEffect', + VALIDATE_FIELD = 'validateField', + VALIDATE_FIELD_CODEGEN = 'validateField', + FORMATTER = 'formatter', + FETCH_BY_PATH = 'fetchByPath', } export const ImportMapping: Record = { @@ -64,4 +69,7 @@ export const ImportMapping: Record = { [ImportValue.USE_AUTH_SIGN_OUT_ACTION]: ImportSource.UI_REACT_INTERNAL, [ImportValue.USE_STATE_MUTATION_ACTION]: ImportSource.UI_REACT_INTERNAL, [ImportValue.USE_EFFECT]: ImportSource.REACT, + [ImportValue.FORMATTER]: ImportSource.UTILS, + [ImportValue.VALIDATE_FIELD]: ImportSource.UTILS, + [ImportValue.FETCH_BY_PATH]: ImportSource.UTILS, }; diff --git a/packages/codegen-ui-react/lib/index.ts b/packages/codegen-ui-react/lib/index.ts index 762909345..25fd4db47 100644 --- a/packages/codegen-ui-react/lib/index.ts +++ b/packages/codegen-ui-react/lib/index.ts @@ -14,6 +14,7 @@ limitations under the License. */ export * from './react-component-renderer'; +export * from './react-table-renderer'; export * from './imports'; export * from './react-studio-template-renderer'; export * from './react-theme-studio-template-renderer'; @@ -21,6 +22,11 @@ export * from './react-output-config'; export * from './react-render-config'; export * from './react-output-manager'; export * from './amplify-ui-renderers/amplify-renderer'; +export * from './amplify-ui-renderers/amplify-form-renderer'; +export * from './amplify-ui-renderers/amplify-view-renderer'; export * from './primitive'; export * from './react-index-studio-template-renderer'; +export * from './react-utils-studio-template-renderer'; export * from './react-required-dependency-provider'; +export * from './utils/forms/validation'; +export { fetchByPath } from './utils/json-path-fetch'; diff --git a/packages/codegen-ui-react/lib/react-component-render-helper.ts b/packages/codegen-ui-react/lib/react-component-render-helper.ts index 66f5115ed..350601aa2 100644 --- a/packages/codegen-ui-react/lib/react-component-render-helper.ts +++ b/packages/codegen-ui-react/lib/react-component-render-helper.ts @@ -52,11 +52,13 @@ import { ArrayLiteralExpression, } from 'typescript'; +import { FormMetadata } from '@aws-amplify/codegen-ui/lib/types'; import { ImportCollection, ImportSource } from './imports'; import { json, jsonToLiteral } from './react-studio-template-renderer-helper'; import { getChildPropMappingForComponentName } from './workflow/utils'; import nameReplacements from './name-replacements'; import keywords from './keywords'; +import { buildAccessChain } from './forms/form-state'; export function getFixedComponentPropValueExpression(prop: FixedStudioComponentProperty): StringLiteral { return factory.createStringLiteral(prop.value.toString(), true); @@ -600,6 +602,40 @@ export function buildOpeningElementProperties( return factory.createJsxAttribute(factory.createIdentifier(name), undefined); } +export function buildLayoutProperties(componentMetadata: FormMetadata | undefined): JsxAttribute[] { + const propMap: Record = { + horizontalGap: 'rowGap', + verticalGap: 'columnGap', + outerPadding: 'padding', + }; + + return Object.entries(componentMetadata?.layoutConfigs ?? {}).reduce((acc, value) => { + const mappedProp = propMap[value[0]]; + + if (!mappedProp) { + return acc; + } + + if (value[1].value) { + acc.push(buildFixedAttr({ value: value[1].value }, mappedProp)); + } else if (value[1].tokenReference) { + const tokenReference = ['tokens', ...value[1].tokenReference.split('.')]; + + acc.push( + factory.createJsxAttribute( + factory.createIdentifier(mappedProp), + factory.createJsxExpression( + undefined, + factory.createPropertyAccessExpression(buildAccessChain(tokenReference, false), 'value'), + ), + ), + ); + } + + return acc; + }, []); +} + export function addBindingPropertiesImports( component: StudioComponent | StudioComponentChild, importCollection: ImportCollection, diff --git a/packages/codegen-ui-react/lib/react-component-renderer.ts b/packages/codegen-ui-react/lib/react-component-renderer.ts index 2541fad78..0802d50a9 100644 --- a/packages/codegen-ui-react/lib/react-component-renderer.ts +++ b/packages/codegen-ui-react/lib/react-component-renderer.ts @@ -43,6 +43,8 @@ import { filterStateReferencesForComponent, } from './workflow'; import { ImportCollection, ImportSource, ImportValue } from './imports'; +import { addFormAttributes } from './forms'; +import { renderArrayFieldComponent } from './utils/forms/array-field-component'; export class ReactComponentRenderer extends ComponentRendererBase< TPropIn, @@ -72,6 +74,15 @@ export class ReactComponentRenderer extends ComponentRendererBase< this.importCollection.addImport(ImportSource.UI_REACT, this.component.componentType); + // Add ArrayField wrapper to element if Array type + if (this.componentMetadata.formMetadata?.fieldConfigs[this.component.name]?.isArray) { + this.importCollection.addImport(ImportSource.UI_REACT, 'Icon'); + this.importCollection.addImport(ImportSource.UI_REACT, 'Badge'); + this.importCollection.addImport(ImportSource.UI_REACT, 'ScrollView'); + this.importCollection.addImport(ImportSource.UI_REACT, 'Divider'); + return renderArrayFieldComponent(this.component.name, element); + } + return element; } @@ -127,6 +138,10 @@ export class ReactComponentRenderer extends ComponentRendererBase< ...controlEventAttributes, ]; + if (this.componentMetadata.formMetadata) { + attributes.push(...addFormAttributes(this.component, this.componentMetadata.formMetadata)); + } + this.addPropsSpreadAttributes(attributes); return factory.createJsxOpeningElement( diff --git a/packages/codegen-ui-react/lib/react-index-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-index-studio-template-renderer.ts index 938f91459..591dae35b 100644 --- a/packages/codegen-ui-react/lib/react-index-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-index-studio-template-renderer.ts @@ -15,15 +15,13 @@ */ import { EOL } from 'os'; import { EmitHint, ExportDeclaration, factory } from 'typescript'; -import { StudioTemplateRenderer, StudioTheme, StudioComponent } from '@aws-amplify/codegen-ui'; +import { StudioTemplateRenderer, StudioSchema } from '@aws-amplify/codegen-ui'; import { ReactRenderConfig, scriptKindToFileExtensionNonReact } from './react-render-config'; import { ImportCollection } from './imports'; import { ReactOutputManager } from './react-output-manager'; import { transpile, buildPrinter, defaultRenderConfig } from './react-studio-template-renderer-helper'; import { RequiredKeys } from './utils/type-utils'; -type StudioSchema = StudioComponent | StudioTheme; - export class ReactIndexStudioTemplateRenderer extends StudioTemplateRenderer< string, StudioSchema[], diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts index 70310150e..cdd093523 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts @@ -21,6 +21,7 @@ import { InternalError, InvalidInputError, StudioComponentSlotBinding, + StudioComponentSort, } from '@aws-amplify/codegen-ui'; import ts, { createPrinter, @@ -38,6 +39,9 @@ import ts, { BindingName, Expression, PropertyAssignment, + ArrowFunction, + CallExpression, + Identifier, } from 'typescript'; import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost } from '@typescript/vfs'; import path from 'path'; @@ -146,6 +150,50 @@ export function buildPrinter(fileName: string, renderConfig: ReactRenderConfig) return { printer, file }; } +/** + * (s: SortPredicate) => s.firstName('ASCENDING').lastName('DESCENDING') + */ +export const buildSortFunction = (model: string, sort: StudioComponentSort[]): ArrowFunction => { + const ascendingSortDirection = factory.createPropertyAccessExpression( + factory.createIdentifier('SortDirection'), + factory.createIdentifier('ASCENDING'), + ); + const descendingSortDirection = factory.createPropertyAccessExpression( + factory.createIdentifier('SortDirection'), + factory.createIdentifier('DESCENDING'), + ); + + let expr: Identifier | CallExpression = factory.createIdentifier('s'); + sort.forEach((sortPredicate) => { + expr = factory.createCallExpression( + factory.createPropertyAccessExpression(expr, factory.createIdentifier(sortPredicate.field)), + undefined, + [sortPredicate.direction === 'ASC' ? ascendingSortDirection : descendingSortDirection], + ); + }); + + return factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('s'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('SortPredicate'), [ + factory.createTypeReferenceNode(factory.createIdentifier(model), undefined), + ]), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + expr, + ); +}; + export function getDeclarationFilename(filename: string): string { return `${path.basename(filename, filename.includes('.tsx') ? '.tsx' : '.jsx')}.d.ts`; } diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts index 17f0d3f38..f8c8c95af 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts @@ -21,6 +21,7 @@ import { isEventPropertyBinding, isStudioComponentWithCollectionProperties, isStudioComponentWithVariants, + isStudioComponentWithBreakpoints, StudioComponent, StudioComponentChild, StudioComponentPredicate, @@ -33,6 +34,7 @@ import { validateComponentSchema, isSlotBinding, GenericDataSchema, + getBreakpoints, } from '@aws-amplify/codegen-ui'; import { EOL } from 'os'; import ts, { @@ -51,13 +53,12 @@ import ts, { Modifier, ObjectLiteralExpression, CallExpression, - Identifier, - ArrowFunction, LiteralExpression, BooleanLiteral, addSyntheticLeadingComment, JsxSelfClosingElement, PropertyAssignment, + ObjectLiteralElementLike, } from 'typescript'; import { ImportCollection, ImportSource, ImportValue } from './imports'; import { ReactOutputManager } from './react-output-manager'; @@ -76,6 +77,7 @@ import { buildPropAssignmentWithFilter, buildCollectionWithItemMap, createHookStatement, + buildSortFunction, } from './react-studio-template-renderer-helper'; import { Primitive, isPrimitive, PrimitiveTypeParameter, PrimitiveChildrenPropMapping } from './primitive'; import { RequiredKeys } from './utils/type-utils'; @@ -543,7 +545,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer return factory.createTypeLiteralNode(propSignatures); } - private buildVariableStatements(component: StudioComponent): Statement[] { + protected buildVariableStatements(component: StudioComponent): Statement[] { const statements: Statement[] = []; const elements: BindingElement[] = []; if (isStudioComponentWithBinding(component)) { @@ -586,6 +588,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer // remove overrides from rest of props const hasVariant = isStudioComponentWithVariants(component); + const hasBreakpoint = isStudioComponentWithBreakpoints(component); elements.push( factory.createBindingElement( undefined, @@ -600,7 +603,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer factory.createBindingElement( factory.createToken(ts.SyntaxKind.DotDotDotToken), undefined, - factory.createIdentifier(hasVariant ? 'restProp' : 'rest'), + factory.createIdentifier(hasBreakpoint ? 'restProp' : 'rest'), undefined, ), ); @@ -624,9 +627,11 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer if (isStudioComponentWithVariants(component)) { this.importCollection.addMappedImport(ImportValue.MERGE_VARIANTS_OVERRIDES); statements.push(this.buildVariantDeclaration(component.variants)); - statements.push(this.buildDefaultBreakpointMap()); - statements.push(this.buildRestWithStyle()); - statements.push(this.buildOverridesFromVariantsAndProp()); + if (hasBreakpoint) { + statements.push(this.buildDefaultBreakpointMap(component)); + statements.push(this.buildRestWithStyle()); + } + statements.push(this.buildOverridesFromVariantsAndProp(hasBreakpoint)); } const authStatement = this.buildUseAuthenticatedUserStatement(); @@ -737,23 +742,24 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer * xxl: 'xxl', * }); */ - private buildDefaultBreakpointMap() { + private buildDefaultBreakpointMap(component: StudioComponent & Required>) { + const breakpoints = getBreakpoints(component); + const element: ObjectLiteralElementLike[] = []; + // if the first element is not base then we sent it anyway as the smallest size should default to base + if (breakpoints[0] !== 'base') { + element.push( + factory.createPropertyAssignment(factory.createIdentifier('base'), factory.createStringLiteral(breakpoints[0])), + ); + } + breakpoints.forEach((bp) => { + element.push(factory.createPropertyAssignment(factory.createIdentifier(bp), factory.createStringLiteral(bp))); + }); this.importCollection.addMappedImport(ImportValue.USE_BREAKPOINT_VALUE); return createHookStatement( 'breakpointHook', 'useBreakpointValue', - factory.createObjectLiteralExpression( - [ - factory.createPropertyAssignment(factory.createIdentifier('base'), factory.createStringLiteral('base')), - factory.createPropertyAssignment(factory.createIdentifier('large'), factory.createStringLiteral('large')), - factory.createPropertyAssignment(factory.createIdentifier('medium'), factory.createStringLiteral('medium')), - factory.createPropertyAssignment(factory.createIdentifier('small'), factory.createStringLiteral('small')), - factory.createPropertyAssignment(factory.createIdentifier('xl'), factory.createStringLiteral('xl')), - factory.createPropertyAssignment(factory.createIdentifier('xxl'), factory.createStringLiteral('xxl')), - ], - true, - ), + factory.createObjectLiteralExpression(element, true), ); } @@ -795,6 +801,8 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer } /** + * If component hasBreakpoint: + * * const overrides = mergeVariantsAndOverrides( * getOverridesFromVariants(variants, { * breakpoint: breakpointHook, @@ -802,8 +810,15 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer * }), * overridesProp || {} * ); + * + * Else: + * + * const overrides = mergeVariantsAndOverrides( + * getOverridesFromVariants(variants, props), + * overridesProp || {} + * ); */ - private buildOverridesFromVariantsAndProp() { + private buildOverridesFromVariantsAndProp(hasBreakpoint: boolean) { this.importCollection.addMappedImport(ImportValue.GET_OVERRIDES_FROM_VARIANTS); this.importCollection.addMappedImport(ImportValue.VARIANT); @@ -818,16 +833,18 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer factory.createCallExpression(factory.createIdentifier('mergeVariantsAndOverrides'), undefined, [ factory.createCallExpression(factory.createIdentifier('getOverridesFromVariants'), undefined, [ factory.createIdentifier('variants'), - factory.createObjectLiteralExpression( - [ - factory.createPropertyAssignment( - factory.createIdentifier('breakpoint'), - factory.createIdentifier('breakpointHook'), - ), - factory.createSpreadAssignment(factory.createIdentifier('props')), - ], - false, - ), + hasBreakpoint + ? factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('breakpoint'), + factory.createIdentifier('breakpointHook'), + ), + factory.createSpreadAssignment(factory.createIdentifier('props')), + ], + false, + ) + : factory.createIdentifier('props'), ]), factory.createBinaryExpression( factory.createIdentifier('overridesProp'), @@ -1046,12 +1063,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer factory.createObjectLiteralExpression( ([] as ts.PropertyAssignment[]).concat( sort - ? [ - factory.createPropertyAssignment( - factory.createIdentifier('sort'), - this.buildSortFunction(model, sort), - ), - ] + ? [factory.createPropertyAssignment(factory.createIdentifier('sort'), buildSortFunction(model, sort))] : [], ), ), @@ -1062,50 +1074,6 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer ); } - /** - * (s: SortPredicate) => s.firstName('ASCENDING').lastName('DESCENDING') - */ - private buildSortFunction(model: string, sort: StudioComponentSort[]): ArrowFunction { - const ascendingSortDirection = factory.createPropertyAccessExpression( - factory.createIdentifier('SortDirection'), - factory.createIdentifier('ASCENDING'), - ); - const descendingSortDirection = factory.createPropertyAccessExpression( - factory.createIdentifier('SortDirection'), - factory.createIdentifier('DESCENDING'), - ); - - let expr: Identifier | CallExpression = factory.createIdentifier('s'); - sort.forEach((sortPredicate) => { - expr = factory.createCallExpression( - factory.createPropertyAccessExpression(expr, factory.createIdentifier(sortPredicate.field)), - undefined, - [sortPredicate.direction === 'ASC' ? ascendingSortDirection : descendingSortDirection], - ); - }); - - return factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('s'), - undefined, - factory.createTypeReferenceNode(factory.createIdentifier('SortPredicate'), [ - factory.createTypeReferenceNode(factory.createIdentifier(model), undefined), - ]), - undefined, - ), - ], - undefined, - factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - expr, - ); - } - private buildCollectionBindingCall( model: string, modelVariableName: string, diff --git a/packages/codegen-ui-react/lib/react-table-renderer-helper.ts b/packages/codegen-ui-react/lib/react-table-renderer-helper.ts new file mode 100644 index 000000000..b52ea9d17 --- /dev/null +++ b/packages/codegen-ui-react/lib/react-table-renderer-helper.ts @@ -0,0 +1,133 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { StringFormat, TableConfiguration, ViewValueFormatting } from '@aws-amplify/codegen-ui/lib/types'; +import { CallExpression, factory, ObjectLiteralExpression, SyntaxKind } from 'typescript'; + +export const getFilterName = (model: string) => `${model.toLowerCase()}Filter`; +export const getPredicateName = (model: string) => `${model.toLowerCase()}Predicate`; +export const getPaginationName = (model: string) => `${model.toLowerCase()}Pagination`; + +/* +checks table to see if there is a formatter for stringFormat +*/ +export const needsFormatter = (config: TableConfiguration): boolean => { + if (config.table.columns) { + return Object.values(config.table.columns).some((column) => column.valueFormatting?.stringFormat !== undefined); + } + return false; +}; + +/* + const dataStoreItems = useDataStoreBinding({ + type: 'collection', + model: 'Post', + criteria: predicateOverrides ?? predicateApiSettings, + sort: sortApiSettings, + }) + */ +export const buildDataStoreCollectionCall = ( + model: string, + criteriaName?: string, + paginationName?: string, +): CallExpression => { + const objectProperties = [ + factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral('collection')), + factory.createPropertyAssignment(factory.createIdentifier('model'), factory.createIdentifier(model)), + ] + .concat( + criteriaName + ? [ + // criteria: predicateOverride || {criteriaName} + factory.createPropertyAssignment( + factory.createIdentifier('criteria'), + factory.createBinaryExpression( + factory.createIdentifier('predicateOverride'), + factory.createToken(SyntaxKind.BarBarToken), + factory.createIdentifier(criteriaName), + ), + ), + ] + : [ + // criteria: predicateOverride + factory.createPropertyAssignment( + factory.createIdentifier('criteria'), + factory.createIdentifier('predicateOverride'), + ), + ], + ) + .concat( + paginationName + ? [ + factory.createPropertyAssignment( + factory.createIdentifier('pagination'), + factory.createIdentifier(paginationName), + ), + ] + : [], + ); + + return factory.createCallExpression(factory.createIdentifier('useDataStoreBinding'), undefined, [ + factory.createObjectLiteralExpression(objectProperties, true), + ]); +}; + +/* Helper to codegen objects + + example output: + { + stringFormat: { + dateTimeFormat: { + dateFormat: "locale", + timeFormat: "hours24", + }, + } + } +*/ +export const objectToExpression = (object: { [key: string]: string | any }): ObjectLiteralExpression => { + return factory.createObjectLiteralExpression( + Object.entries(object).map(([key, value]) => + factory.createPropertyAssignment( + factory.createIdentifier(key), + typeof value === 'string' ? factory.createStringLiteral(value) : objectToExpression(value), + ), + ), + ); +}; + +export const stringFormatToType = (viewFormat: ViewValueFormatting | undefined): StringFormat['type'] => { + if (!viewFormat) { + return undefined; + } + + if (viewFormat.stringFormat.type) { + return viewFormat.stringFormat.type; + } + + const { type, ...format } = viewFormat.stringFormat; + switch (Object.keys(format)[0]) { + case 'nonLocaleDateTimeFormat': + return 'NonLocaleDateTimeFormat'; + case 'localeDateTimeFormat': + return 'LocaleDateTimeFormat'; + case 'timeFormat': + return 'TimeFormat'; + case 'dateFormat': + return 'DateFormat'; + default: + // Unsupported formatting + return undefined; + } +}; diff --git a/packages/codegen-ui-react/lib/react-table-renderer.ts b/packages/codegen-ui-react/lib/react-table-renderer.ts new file mode 100644 index 000000000..0fbf346b2 --- /dev/null +++ b/packages/codegen-ui-react/lib/react-table-renderer.ts @@ -0,0 +1,320 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { ColumnInfo, StringFormat, StudioView, TableDefinition, ViewMetadata } from '@aws-amplify/codegen-ui'; +import { + factory, + Identifier, + JsxAttribute, + JsxAttributes, + JsxChild, + JsxElement, + JsxExpression, + JsxOpeningElement, + ObjectLiteralExpression, + SyntaxKind, +} from 'typescript'; +import { ImportCollection, ImportSource } from './imports'; +import { Primitive } from './primitive'; +import { objectToExpression, stringFormatToType } from './react-table-renderer-helper'; + +export class ReactTableRenderer { + private requiredUIReactImports = [ + Primitive.Table, + Primitive.TableHead, + Primitive.TableBody, + Primitive.TableRow, + Primitive.TableCell, + Primitive.TableFoot, + ]; + + protected viewDefinition: TableDefinition; + + protected viewComponent: StudioView; + + protected viewMetadata: ViewMetadata; + + constructor(view: StudioView, definition: TableDefinition, metadata: ViewMetadata, imports: ImportCollection) { + this.viewComponent = view; + this.viewDefinition = definition; + this.viewMetadata = metadata; + this.viewMetadata.fieldFormatting = {}; + + this.viewDefinition.columns.forEach((column) => { + if (column.valueFormatting) { + this.viewMetadata.fieldFormatting[column.header] = { ...column.valueFormatting }; + } + }); + + this.requiredUIReactImports.forEach((importName) => { + imports.addImport(ImportSource.UI_REACT, importName); + }); + } + + createOpeningTableElement(): JsxOpeningElement { + const tableAttributes: JsxAttribute[] = []; + + if (this.viewDefinition.tableConfig.table.highlightOnHover) { + tableAttributes.push( + factory.createJsxAttribute( + factory.createIdentifier('highlightOnHover'), + factory.createJsxExpression(undefined, factory.createIdentifier('highlightOnHover')), + ), + ); + } + + return factory.createJsxOpeningElement( + factory.createIdentifier(Primitive.Table), + undefined, + factory.createJsxAttributes(tableAttributes), + ); + } + + createTableRow(children: JsxChild[], attributes: JsxAttributes): JsxElement { + return factory.createJsxElement( + factory.createJsxOpeningElement(factory.createIdentifier(Primitive.TableRow), undefined, attributes), + children, + factory.createJsxClosingElement(factory.createIdentifier(Primitive.TableRow)), + ); + } + + createTableHeadRow(): JsxChild { + const cellsInHeader: JsxChild[] = this.viewDefinition.columns.map((column) => { + return factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier(Primitive.TableCell), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute(factory.createIdentifier('as'), factory.createStringLiteral('th')), + ]), + ), + [factory.createJsxText(column.label ?? column.header, false)], + factory.createJsxClosingElement(factory.createIdentifier(Primitive.TableCell)), + ); + }); + + return this.createTableRow(cellsInHeader, factory.createJsxAttributes([])); + } + + createTableHeadElement(): JsxElement { + return factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier(Primitive.TableHead), + undefined, + factory.createJsxAttributes([]), + ), + [this.createTableHeadRow()], + factory.createJsxClosingElement(factory.createIdentifier(Primitive.TableHead)), + ); + } + + createTableHeadElementBlock(): JsxExpression { + return factory.createJsxExpression( + undefined, + factory.createBinaryExpression( + factory.createPrefixUnaryExpression(SyntaxKind.ExclamationToken, factory.createIdentifier('disableHeaders')), + SyntaxKind.AmpersandAmpersandToken, + this.createTableHeadElement(), + ), + ); + } + + createRowOnClickCBAttr(): JsxAttributes { + return factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression( + undefined, + factory.createConditionalExpression( + factory.createIdentifier('onRowClick'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression(factory.createIdentifier('onRowClick'), undefined, [ + factory.createIdentifier('item'), + factory.createIdentifier('index'), + ]), + ), + factory.createToken(SyntaxKind.ColonToken), + factory.createNull(), + ), + ), + ), + ]); + } + + /* Expected arg shape examples: + For dateTime: + { + type: 'NonLocaleDateTimeFormat' + format: { + nonLocaleDateTimeFormat: { + dateFormat: 'locale', + timeFormat: 'hours24', + } + } + } + For date: + { + type: 'DateFormat' + format: { + dateFormat: 'Mmm, DD YYYY', + } + } + */ + generateFormatLiteralExpression(field: string): ObjectLiteralExpression | Identifier { + const formatting = this.viewMetadata.fieldFormatting; + + if (formatting?.[field]) { + return objectToExpression(formatting[field].stringFormat); + } + return factory.createIdentifier('undefined'); + } + + createFieldAccessExpression(identifier: string, field: string) { + return factory.createPropertyAccessChain( + factory.createIdentifier(identifier), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier(field), + ); + } + + createFormatArg(field: string) { + const format = this.viewMetadata.fieldFormatting?.[field]; + + const type: StringFormat['type'] | undefined = stringFormatToType(format); + + if (format && type) { + return factory.createObjectLiteralExpression([ + factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral(type)), + factory.createPropertyAssignment( + factory.createIdentifier('format'), + this.generateFormatLiteralExpression(field), + ), + ]); + } + + return undefined; + } + + createFormatCallOrPropAccess(field: string) { + const formatterArg = this.createFormatArg(field); + return formatterArg + ? factory.createCallExpression(factory.createIdentifier('formatter'), undefined, [ + this.createFieldAccessExpression('item', field), + formatterArg, + ]) + : this.createFieldAccessExpression('item', field); + } + + createTableBodyCellFromColumn(column: ColumnInfo): JsxElement { + const columnId = column.header; + + return factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier(Primitive.TableCell), + undefined, + factory.createJsxAttributes([]), + ), + [ + factory.createJsxExpression( + undefined, + factory.createConditionalExpression( + this.createFieldAccessExpression('format', columnId), + factory.createToken(SyntaxKind.QuestionToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('format'), + factory.createIdentifier(columnId), + ), + undefined, + [this.createFieldAccessExpression('item', columnId)], + ), + factory.createToken(SyntaxKind.ColonToken), + this.createFormatCallOrPropAccess(columnId), + ), + ), + ], + factory.createJsxClosingElement(factory.createIdentifier(Primitive.TableCell)), + ); + } + + createTableBodyRow(): JsxElement { + const tableBodyCells: JsxChild[] = this.viewDefinition.columns.map((column) => + this.createTableBodyCellFromColumn(column), + ); + return this.createTableRow(tableBodyCells, this.createRowOnClickCBAttr()); + } + + createTableBodyElement(): JsxChild { + const itemParam = factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('item'), + undefined, + undefined, + undefined, + ); + const indexParam = factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('index'), + undefined, + undefined, + undefined, + ); + return factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier(Primitive.TableBody), + undefined, + factory.createJsxAttributes([]), + ), + [ + factory.createJsxExpression( + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier('items'), factory.createIdentifier('map')), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [itemParam, indexParam], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createParenthesizedExpression(this.createTableBodyRow()), + ), + ], + ), + ), + ], + factory.createJsxClosingElement(factory.createIdentifier(Primitive.TableBody)), + ); + } + + renderElement(): JsxElement { + return factory.createJsxElement( + this.createOpeningTableElement(), + [this.createTableHeadElementBlock(), this.createTableBodyElement()], + factory.createJsxClosingElement(factory.createIdentifier(Primitive.Table)), + ); + } +} diff --git a/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts new file mode 100644 index 000000000..b4f360e38 --- /dev/null +++ b/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts @@ -0,0 +1,107 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { EOL } from 'os'; +import ts, { EmitHint } from 'typescript'; +import { StudioTemplateRenderer } from '@aws-amplify/codegen-ui'; +import { ReactRenderConfig, scriptKindToFileExtensionNonReact } from './react-render-config'; +import { ImportCollection } from './imports'; +import { ReactOutputManager } from './react-output-manager'; +import { RequiredKeys } from './utils/type-utils'; +import { transpile, buildPrinter, defaultRenderConfig } from './react-studio-template-renderer-helper'; +import { generateValidationFunction } from './utils/forms/validation'; +import { getFetchByPathNodeFunction } from './utils/json-path-fetch'; +import { generateFormatUtil } from './utils/string-formatter'; + +export type UtilTemplateType = 'validation' | 'formatter' | 'fetchByPath'; + +export class ReactUtilsStudioTemplateRenderer extends StudioTemplateRenderer< + string, + UtilTemplateType[], + ReactOutputManager, + { + componentText: string; + renderComponentToFilesystem: (outputPath: string) => Promise; + } +> { + protected importCollection = new ImportCollection(); + + protected renderConfig: RequiredKeys; + + fileName: string; + + /* + * list of util functions to generate + */ + utils: UtilTemplateType[]; + + constructor(utils: UtilTemplateType[], renderConfig: ReactRenderConfig) { + super(utils, new ReactOutputManager(), renderConfig); + this.utils = utils; + this.renderConfig = { + ...defaultRenderConfig, + ...renderConfig, + renderTypeDeclarations: false, // Never render type declarations for index.js|ts file. + }; + this.fileName = `utils.${scriptKindToFileExtensionNonReact(this.renderConfig.script)}`; + } + + renderComponentInternal() { + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + const utilsStatements: (ts.VariableStatement | ts.TypeAliasDeclaration | ts.FunctionDeclaration)[] = []; + const skipReactImport = true; + + const utilsSet = new Set(this.utils); + + if (utilsSet.has('validation')) { + utilsStatements.push(...generateValidationFunction()); + } + + if (utilsSet.has('formatter')) { + utilsStatements.push(...generateFormatUtil()); + } + + if (utilsSet.has('fetchByPath')) { + utilsStatements.push(getFetchByPathNodeFunction()); + } + + let componentText = `/* eslint-disable */${EOL}`; + const imports = this.importCollection.buildImportStatements(skipReactImport); + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + componentText += result + EOL; + }); + componentText += EOL; + + utilsStatements.forEach((util) => { + const result = printer.printNode(EmitHint.Unspecified, util, file); + componentText += result + EOL; + }); + + componentText += EOL; + + const { componentText: transpliedText } = transpile(componentText, this.renderConfig); + + return { + componentText: transpliedText, + renderComponentToFilesystem: async (outputPath: string) => { + await this.renderComponentToFilesystem(transpliedText)(this.fileName)(outputPath); + }, + }; + } + + // no-op + validateSchema() {} +} diff --git a/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts b/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts new file mode 100644 index 000000000..9d2260b61 --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/forms/array-field-component.ts @@ -0,0 +1,844 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { factory, JsxChild, JsxTagNamePropertyAccess, NodeFlags, SyntaxKind } from 'typescript'; +import { + capitalizeFirstLetter, + getCurrentValueIdentifier, + getCurrentValueName, + getSetNameIdentifier, +} from '../../forms/form-state'; +import { lowerCaseFirst } from '../../helpers'; + +export const generateArrayFieldComponent = () => { + const iconPath = 'M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z'; + return factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('ArrayField'), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createObjectBindingPattern([ + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier('items'), + factory.createArrayLiteralExpression([], false), + ), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onChange'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('inputFieldRef'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('children'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('hasError'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('setFieldValue'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('currentFieldValue'), undefined), + ]), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createArrayBindingPattern([ + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier('selectedBadgeIndex'), + undefined, + ), + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier('setSelectedBadgeIndex'), + undefined, + ), + ]), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('React'), + factory.createIdentifier('useState'), + ), + undefined, + [], + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('removeItem'), + undefined, + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('removeIndex'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('newItems'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('items'), + factory.createIdentifier('filter'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('index'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBinaryExpression( + factory.createIdentifier('index'), + factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), + factory.createIdentifier('removeIndex'), + ), + ), + ], + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression(factory.createIdentifier('onChange'), undefined, [ + factory.createIdentifier('newItems'), + ]), + ), + ), + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier('setSelectedBadgeIndex'), undefined, [ + factory.createIdentifier('undefined'), + ]), + ), + ], + true, + ), + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('addItem'), + undefined, + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('currentFieldValue'), + factory.createIdentifier('length'), + ), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createIdentifier('hasError'), + ), + ), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('newItems'), + undefined, + undefined, + factory.createArrayLiteralExpression( + [factory.createSpreadElement(factory.createIdentifier('items'))], + false, + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createIdentifier('selectedBadgeIndex'), + factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createElementAccessExpression( + factory.createIdentifier('newItems'), + factory.createIdentifier('selectedBadgeIndex'), + ), + factory.createToken(SyntaxKind.EqualsToken), + factory.createIdentifier('currentFieldValue'), + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier('setSelectedBadgeIndex'), + undefined, + [factory.createIdentifier('undefined')], + ), + ), + ], + true, + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('newItems'), + factory.createIdentifier('push'), + ), + undefined, + [factory.createIdentifier('currentFieldValue')], + ), + ), + ], + true, + ), + ), + factory.createExpressionStatement( + factory.createAwaitExpression( + factory.createCallExpression(factory.createIdentifier('onChange'), undefined, [ + factory.createIdentifier('newItems'), + ]), + ), + ), + ], + true, + ), + undefined, + ), + ], + true, + ), + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createParenthesizedExpression( + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createPropertyAccessExpression( + factory.createIdentifier('React'), + factory.createIdentifier('Fragment'), + ) as JsxTagNamePropertyAccess, + undefined, + factory.createJsxAttributes([]), + ), + [ + factory.createJsxExpression(undefined, factory.createIdentifier('children')), + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('Flex'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('justifyContent'), + factory.createStringLiteral('flex-end'), + ), + ]), + ), + [ + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('Button'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('children'), + factory.createStringLiteral('Cancel'), + ), + factory.createJsxAttribute( + factory.createIdentifier('type'), + factory.createStringLiteral('button'), + ), + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier('setFieldValue'), + undefined, + [factory.createStringLiteral('')], + ), + ), + ], + true, + ), + ), + ), + ), + ]), + ), + [], + factory.createJsxClosingElement(factory.createIdentifier('Button')), + ), + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('Button'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('children'), + factory.createStringLiteral('Save'), + ), + factory.createJsxAttribute( + factory.createIdentifier('variation'), + factory.createStringLiteral('primary'), + ), + factory.createJsxAttribute( + factory.createIdentifier('isDisabled'), + factory.createJsxExpression(undefined, factory.createIdentifier('hasError')), + ), + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression(undefined, factory.createIdentifier('addItem')), + ), + ]), + ), + [], + factory.createJsxClosingElement(factory.createIdentifier('Button')), + ), + ], + factory.createJsxClosingElement(factory.createIdentifier('Flex')), + ), + factory.createJsxExpression( + undefined, + factory.createBinaryExpression( + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createPropertyAccessExpression( + factory.createIdentifier('items'), + factory.createIdentifier('length'), + ), + ), + ), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('ScrollView'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('height'), + factory.createStringLiteral('inherit'), + ), + factory.createJsxAttribute( + factory.createIdentifier('width'), + factory.createStringLiteral('inherit'), + ), + factory.createJsxAttribute( + factory.createIdentifier('maxHeight'), + factory.createJsxExpression(undefined, factory.createStringLiteral('7rem')), + ), + ]), + ), + [ + factory.createJsxExpression( + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('items'), + factory.createIdentifier('map'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('index'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createReturnStatement( + factory.createParenthesizedExpression( + factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('Badge'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('key'), + factory.createJsxExpression( + undefined, + factory.createIdentifier('index'), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('style'), + factory.createJsxExpression( + undefined, + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('cursor'), + factory.createStringLiteral('pointer'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('alignItems'), + factory.createStringLiteral('center'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('marginRight'), + factory.createNumericLiteral('3'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('marginTop'), + factory.createNumericLiteral('3'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('backgroundColor'), + factory.createConditionalExpression( + factory.createBinaryExpression( + factory.createIdentifier('index'), + factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('selectedBadgeIndex'), + ), + factory.createToken(SyntaxKind.QuestionToken), + factory.createStringLiteral('#B8CEF9'), + factory.createToken(SyntaxKind.ColonToken), + factory.createStringLiteral(''), + ), + ), + ], + true, + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier('setSelectedBadgeIndex'), + undefined, + [factory.createIdentifier('index')], + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier('setFieldValue'), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('items'), + factory.createIdentifier('index'), + ), + ], + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessChain( + factory.createPropertyAccessChain( + factory.createIdentifier('inputFieldRef'), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('current'), + ), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('focus'), + ), + undefined, + [], + ), + ), + ], + true, + ), + ), + ), + ), + ]), + ), + [ + factory.createJsxExpression(undefined, factory.createIdentifier('value')), + factory.createJsxSelfClosingElement( + factory.createIdentifier('Icon'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('style'), + factory.createJsxExpression( + undefined, + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('cursor'), + factory.createStringLiteral('pointer'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('paddingLeft'), + factory.createNumericLiteral('3'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('width'), + factory.createNumericLiteral('20'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('height'), + factory.createNumericLiteral('20'), + ), + ], + true, + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('viewBox'), + factory.createJsxExpression( + undefined, + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('width'), + factory.createNumericLiteral('20'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('height'), + factory.createNumericLiteral('20'), + ), + ], + false, + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('paths'), + factory.createJsxExpression( + undefined, + factory.createArrayLiteralExpression( + [ + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('d'), + factory.createStringLiteral(iconPath), + ), + factory.createPropertyAssignment( + factory.createIdentifier('stroke'), + factory.createStringLiteral('black'), + ), + ], + true, + ), + ], + true, + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('ariaLabel'), + factory.createStringLiteral('button'), + ), + factory.createJsxAttribute( + factory.createIdentifier('onClick'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('event'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('event'), + factory.createIdentifier('stopPropagation'), + ), + undefined, + [], + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier('removeItem'), + undefined, + [factory.createIdentifier('index')], + ), + ), + ], + true, + ), + ), + ), + ), + ]), + ), + ], + factory.createJsxClosingElement(factory.createIdentifier('Badge')), + ), + ), + ), + ], + true, + ), + ), + ], + ), + ), + ], + factory.createJsxClosingElement(factory.createIdentifier('ScrollView')), + ), + ), + ), + factory.createJsxSelfClosingElement( + factory.createIdentifier('Divider'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('orientation'), + factory.createStringLiteral('horizontal'), + ), + factory.createJsxAttribute( + factory.createIdentifier('marginTop'), + factory.createJsxExpression(undefined, factory.createNumericLiteral('5')), + ), + ]), + ), + ], + factory.createJsxClosingElement( + factory.createPropertyAccessExpression( + factory.createIdentifier('React'), + factory.createIdentifier('Fragment'), + ) as JsxTagNamePropertyAccess, + ), + ), + ), + ), + ], + true, + ), + ); +}; +/* + { + setBreeds(items); + setCurrentBreedsValue(''); + }} + currentBreedsValue = { currentBreedsValue } + hasError = { errors.breeds?.hasError } + setFieldValue = { setCurrentBreedsValue } + inputFieldRef={ breedsRef } + > + + + */ + +export const renderArrayFieldComponent = (fieldName: string, inputField: JsxChild) => { + const stateName = getCurrentValueIdentifier(fieldName); + const setStateName = getSetNameIdentifier(getCurrentValueName(fieldName)); + return factory.createJsxElement( + factory.createJsxOpeningElement( + factory.createIdentifier('ArrayField'), + undefined, + factory.createJsxAttributes([ + factory.createJsxAttribute( + factory.createIdentifier('onChange'), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + [factory.createModifier(SyntaxKind.AsyncKeyword)], + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('items'), + undefined, + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier(`set${capitalizeFirstLetter(fieldName)}`), + undefined, + [factory.createIdentifier('items')], + ), + ), + factory.createExpressionStatement( + factory.createCallExpression(setStateName, undefined, [factory.createStringLiteral('')]), + ), + ], + true, + ), + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier(`currentFieldValue`), + factory.createJsxExpression(undefined, stateName), + ), + factory.createJsxAttribute( + factory.createIdentifier('items'), + factory.createJsxExpression(undefined, factory.createIdentifier(fieldName)), + ), + factory.createJsxAttribute( + factory.createIdentifier('hasError'), + factory.createJsxExpression( + undefined, + factory.createPropertyAccessChain( + factory.createPropertyAccessExpression( + factory.createIdentifier('errors'), + factory.createIdentifier(fieldName), + ), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + ), + ), + factory.createJsxAttribute( + factory.createIdentifier('setFieldValue'), + factory.createJsxExpression(undefined, setStateName), + ), + factory.createJsxAttribute( + factory.createIdentifier('inputFieldRef'), + factory.createJsxExpression(undefined, factory.createIdentifier(`${lowerCaseFirst(fieldName)}Ref`)), + ), + ]), + ), + [inputField], + factory.createJsxClosingElement(factory.createIdentifier('ArrayField')), + ); +}; diff --git a/packages/codegen-ui-react/lib/utils/forms/layout-helpers.ts b/packages/codegen-ui-react/lib/utils/forms/layout-helpers.ts new file mode 100644 index 000000000..cf6d89d7f --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/forms/layout-helpers.ts @@ -0,0 +1,25 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { ComponentMetadata } from '@aws-amplify/codegen-ui'; + +/** + * Detemrines if a form has any layout configs to conditionally render theme hooks + */ +export function hasTokenReference(componentMetadata: ComponentMetadata): boolean { + return Object.values(componentMetadata.formMetadata?.layoutConfigs ?? {}).some((config) => { + return config.tokenReference; + }); +} diff --git a/packages/codegen-ui-react/lib/utils/forms/validation.ts b/packages/codegen-ui-react/lib/utils/forms/validation.ts index 1d7db9bc8..a2d0d31f2 100644 --- a/packages/codegen-ui-react/lib/utils/forms/validation.ts +++ b/packages/codegen-ui-react/lib/utils/forms/validation.ts @@ -1,121 +1,1686 @@ /* eslint-disable */ -import { - FieldValidationConfiguration, - ValidationResponse, - ValidationTypes, -} from '@aws-amplify/codegen-ui/lib/types/form/form-validation'; +import ts, { factory } from 'typescript'; + +type ValidationResponse = { hasError: boolean; errorMessage?: string }; +type FieldValidationConfiguration = { + type: string; + strValues?: string[]; + numValues?: number[]; + validationMessage?: string; +}; export const validateField = (value: any, validations: FieldValidationConfiguration[]): ValidationResponse => { for (const validation of validations) { - switch (validation.type) { - case ValidationTypes.REQUIRED: + if (value === undefined || value === '') { + if (validation.type === 'Required') { return { - hasError: value === undefined || value === '', + hasError: true, errorMessage: validation.validationMessage || 'The value is required', }; - case ValidationTypes.START_WITH: - return { - hasError: !validation.values.some((el) => value.startsWith(el)), - errorMessage: validation.validationMessage || `The value must start with ${validation.values.join(', ')}`, - }; - case ValidationTypes.END_WITH: - return { - hasError: !validation.values.some((el) => value.endsWith(el)), - errorMessage: validation.validationMessage || `The value must end with ${validation.values.join(', ')}`, - }; - case ValidationTypes.CONTAINS: + } else { return { - hasError: !validation.values.some((el) => value.includes(el)), - errorMessage: validation.validationMessage || `The value must contain ${validation.values.join(', ')}`, + hasError: false, }; - case ValidationTypes.NOT_CONTAINS: - return { - hasError: !validation.values.every((el) => !value.includes(el)), - errorMessage: validation.validationMessage || `The value must not contain ${validation.values.join(', ')}`, - }; - case ValidationTypes.LESS_THAN_CHAR_LENGTH: + } + } + + const validationResult = checkValidation(value, validation); + + if (validationResult?.hasError) { + return validationResult; + } + } + return { hasError: false }; +}; + +const checkValidation = (value: any, validation: FieldValidationConfiguration) => { + if (validation.numValues?.length) { + switch (validation.type) { + case 'LessThanChar': return { - hasError: !(value.length < validation.values), - errorMessage: validation.validationMessage || `The value must be shorter than ${validation.values}`, + hasError: !(value.length < validation.numValues[0]), + errorMessage: validation.validationMessage || `The value must be shorter than ${validation.numValues[0]}`, }; - case ValidationTypes.GREATER_THAN_CHAR_LENGTH: + case 'GreaterThanChar': return { - hasError: !(value.length > validation.values), - errorMessage: validation.validationMessage || `The value must be longer than ${validation.values}`, + hasError: !(value.length > validation.numValues[0]), + errorMessage: validation.validationMessage || `The value must be longer than ${validation.numValues[0]}`, }; - case ValidationTypes.LESS_THAN_NUM: + case 'LessThanNum': return { - hasError: !(value < validation.values), - errorMessage: validation.validationMessage || `The value must be less than ${validation.values}`, + hasError: !(value < validation.numValues[0]), + errorMessage: validation.validationMessage || `The value must be less than ${validation.numValues[0]}`, }; - case ValidationTypes.GREATER_THAN_NUM: + case 'GreaterThanNum': return { - hasError: !(value > validation.values), - errorMessage: validation.validationMessage || `The value must be greater than ${validation.values}`, + hasError: !(value > validation.numValues[0]), + errorMessage: validation.validationMessage || `The value must be greater than ${validation.numValues[0]}`, }; - case ValidationTypes.EQUAL_TO_NUM: - if (Array.isArray(validation.values)) { - return { - hasError: !validation.values.some((el) => el === value), - errorMessage: - validation.validationMessage || `The value must be equal to ${validation.values.join(' or ')}`, - }; - } + case 'EqualTo': return { - hasError: !(value === validation.values), - errorMessage: validation.validationMessage || `The value must be equal to ${validation.values}`, + hasError: !validation.numValues.some((el) => el === value), + errorMessage: + validation.validationMessage || `The value must be equal to ${validation.numValues.join(' or ')}`, }; - case ValidationTypes.BE_AFTER: + default: + } + } else if (validation.strValues?.length) { + switch (validation.type) { + case 'StartWith': return { - hasError: !(new Date(value) > new Date(validation.values)), - errorMessage: validation.validationMessage || `The value must be after ${validation.values}`, + hasError: !validation.strValues.some((el: any) => value.startsWith(el)), + errorMessage: validation.validationMessage || `The value must start with ${validation.strValues.join(', ')}`, }; - case ValidationTypes.BE_BEFORE: + case 'EndWith': return { - hasError: !(new Date(value) < new Date(validation.values)), - errorMessage: validation.validationMessage || `The value must be before ${validation.values}`, + hasError: !validation.strValues.some((el: any) => value.endsWith(el)), + errorMessage: validation.validationMessage || `The value must end with ${validation.strValues.join(', ')}`, }; - case ValidationTypes.EMAIL: - const EMAIL_ADDRESS_REGEX = - /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; + case 'Contains': return { - hasError: !EMAIL_ADDRESS_REGEX.test(value), - errorMessage: validation.validationMessage || 'The value must be a valid email address', + hasError: !validation.strValues.some((el: any) => value.includes(el)), + errorMessage: validation.validationMessage || `The value must contain ${validation.strValues.join(', ')}`, }; - case ValidationTypes.JSON: - let isInvalidJSON = false; - try { - JSON.parse(value); - } catch (e) { - isInvalidJSON = true; - } + case 'NotContains': return { - hasError: isInvalidJSON, - errorMessage: validation.validationMessage || 'The value must be in a correct JSON format', + hasError: !validation.strValues.every((el: any) => !value.includes(el)), + errorMessage: validation.validationMessage || `The value must not contain ${validation.strValues.join(', ')}`, }; - case ValidationTypes.IP_ADDRESS: - const IPV_4 = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/; - const IPV_6 = - /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/; + case 'BeAfter': + const afterTimeValue = parseInt(validation.strValues[0]); + const afterTimeValidator = Number.isNaN(afterTimeValue) ? validation.strValues[0] : afterTimeValue; return { - hasError: !(IPV_4.test(value) || IPV_6.test(value)), - errorMessage: validation.validationMessage || 'The value must be an IPv4 or IPv6 address', + hasError: !(new Date(value) > new Date(afterTimeValidator)), + errorMessage: validation.validationMessage || `The value must be after ${validation.strValues[0]}`, }; - case ValidationTypes.URL: - let isInvalidUrl = false; - try { - new URL(value); - } catch (e) { - isInvalidUrl = true; - } + case 'BeBefore': + const beforeTimeValue = parseInt(validation.strValues[0]); + const beforeTimevalue = Number.isNaN(beforeTimeValue) ? validation.strValues[0] : beforeTimeValue; return { - hasError: isInvalidUrl, - errorMessage: - validation.validationMessage || - 'The value must be a valid URL that begins with a schema (i.e. http:// or mailto:)', + hasError: !(new Date(value) < new Date(beforeTimevalue)), + errorMessage: validation.validationMessage || `The value must be before ${validation.strValues[0]}`, }; - default: } } - return { hasError: false }; + switch (validation.type) { + case 'Email': + const EMAIL_ADDRESS_REGEX = + /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; + return { + hasError: !EMAIL_ADDRESS_REGEX.test(value), + errorMessage: validation.validationMessage || 'The value must be a valid email address', + }; + case 'JSON': + let isInvalidJSON = false; + try { + JSON.parse(value); + } catch (e) { + isInvalidJSON = true; + } + return { + hasError: isInvalidJSON, + errorMessage: validation.validationMessage || 'The value must be in a correct JSON format', + }; + case 'IpAddress': + const IPV_4 = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/; + const IPV_6 = + /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/; + return { + hasError: !(IPV_4.test(value) || IPV_6.test(value)), + errorMessage: validation.validationMessage || 'The value must be an IPv4 or IPv6 address', + }; + case 'URL': + let isInvalidUrl = false; + try { + new URL(value); + } catch (e) { + isInvalidUrl = true; + } + return { + hasError: isInvalidUrl, + errorMessage: + validation.validationMessage || + 'The value must be a valid URL that begins with a schema (i.e. http:// or mailto:)', + }; + case 'Phone': + const PHONE = /^\+?\d[\d\s-]+$/; + return { + hasError: !PHONE.test(value), + errorMessage: validation.validationMessage || 'The value must be a valid phone number', + }; + default: + } +}; + +// AST-viewer does not escape backslashes, so it generates the wrong regex +const EscapedRegexLiterals = { + emailAddress: + "/^[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~](\\.?[-!#$%&'*+\\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\\.?[a-zA-Z0-9])*\\.[a-zA-Z](-?[a-zA-Z0-9])+$/", + ipv4: '/^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$/', + ipv6: '/^(?:(?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/', + phone: '/^\\+?\\d[\\d\\s-]+$/', +}; + +export const generateValidationFunction = () => { + return [ + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('ValidationResponse'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('hasError'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('errorMessage'), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ]), + ), + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('FieldValidationConfiguration'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('type'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('strValues'), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createArrayTypeNode(factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('numValues'), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createArrayTypeNode(factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('validationMessage'), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ]), + ), + factory.createVariableStatement( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validateField'), + undefined, + undefined, + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('validations'), + undefined, + factory.createArrayTypeNode( + factory.createTypeReferenceNode( + factory.createIdentifier('FieldValidationConfiguration'), + undefined, + ), + ), + undefined, + ), + ], + factory.createTypeReferenceNode(factory.createIdentifier('ValidationResponse'), undefined), + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createForOfStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validation'), + undefined, + undefined, + undefined, + ), + ], + ts.NodeFlags.Const, + ), + factory.createIdentifier('validations'), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createBinaryExpression( + factory.createIdentifier('value'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createBinaryExpression( + factory.createIdentifier('value'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createStringLiteral(''), + ), + ), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('type'), + ), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createStringLiteral('Required'), + ), + factory.createBlock( + [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createTrue(), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral('The value is required'), + ), + ), + ], + true, + ), + ), + ], + true, + ), + factory.createBlock( + [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createFalse(), + ), + ], + true, + ), + ), + ], + true, + ), + ), + ], + true, + ), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validationResult'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('checkValidation'), undefined, [ + factory.createIdentifier('value'), + factory.createIdentifier('validation'), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createPropertyAccessChain( + factory.createIdentifier('validationResult'), + factory.createToken(ts.SyntaxKind.QuestionDotToken), + factory.createIdentifier('hasError'), + ), + factory.createBlock( + [factory.createReturnStatement(factory.createIdentifier('validationResult'))], + true, + ), + undefined, + ), + ], + true, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [factory.createPropertyAssignment(factory.createIdentifier('hasError'), factory.createFalse())], + false, + ), + ), + ], + true, + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('checkValidation'), + undefined, + undefined, + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('validation'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('FieldValidationConfiguration'), undefined), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createIfStatement( + factory.createPropertyAccessChain( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createToken(ts.SyntaxKind.QuestionDotToken), + factory.createIdentifier('length'), + ), + factory.createBlock( + [ + factory.createSwitchStatement( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('type'), + ), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('LessThanChar'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('length'), + ), + factory.createToken(ts.SyntaxKind.LessThanToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be shorter than ', + 'The value must be shorter than ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('GreaterThanChar'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('length'), + ), + factory.createToken(ts.SyntaxKind.GreaterThanToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be longer than ', + 'The value must be longer than ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('LessThanNum'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createIdentifier('value'), + factory.createToken(ts.SyntaxKind.LessThanToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be less than ', + 'The value must be less than ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('GreaterThanNum'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createIdentifier('value'), + factory.createToken(ts.SyntaxKind.GreaterThanToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be greater than ', + 'The value must be greater than ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('EqualTo'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('el'), + undefined, + undefined, + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createBinaryExpression( + factory.createIdentifier('el'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('value'), + ), + ), + ], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be equal to ', + 'The value must be equal to ', + ), + [ + factory.createTemplateSpan( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('numValues'), + ), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral(' or ')], + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createDefaultClause([]), + ]), + ), + ], + true, + ), + factory.createIfStatement( + factory.createPropertyAccessChain( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createToken(ts.SyntaxKind.QuestionDotToken), + factory.createIdentifier('length'), + ), + factory.createBlock( + [ + factory.createSwitchStatement( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('type'), + ), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('StartWith'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('el'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('startsWith'), + ), + undefined, + [factory.createIdentifier('el')], + ), + ), + ], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must start with ', + 'The value must start with ', + ), + [ + factory.createTemplateSpan( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral(', ')], + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('EndWith'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('el'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('endsWith'), + ), + undefined, + [factory.createIdentifier('el')], + ), + ), + ], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must end with ', + 'The value must end with ', + ), + [ + factory.createTemplateSpan( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral(', ')], + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('Contains'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('some'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('el'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('includes'), + ), + undefined, + [factory.createIdentifier('el')], + ), + ), + ], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must contain ', + 'The value must contain ', + ), + [ + factory.createTemplateSpan( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral(', ')], + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('NotContains'), [ + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('every'), + ), + undefined, + [ + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('el'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('value'), + factory.createIdentifier('includes'), + ), + undefined, + [factory.createIdentifier('el')], + ), + ), + ), + ], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must not contain ', + 'The value must not contain ', + ), + [ + factory.createTemplateSpan( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral(', ')], + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('BeAfter'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('afterTimeValue'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('parseInt'), undefined, [ + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('afterTimeValidator'), + undefined, + undefined, + factory.createConditionalExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('isNaN'), + ), + undefined, + [factory.createIdentifier('afterTimeValue')], + ), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createToken(ts.SyntaxKind.ColonToken), + factory.createIdentifier('afterTimeValue'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createIdentifier('value'), + ]), + factory.createToken(ts.SyntaxKind.GreaterThanToken), + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createIdentifier('afterTimeValidator'), + ]), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be after ', + 'The value must be after ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('BeBefore'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('beforeTimeValue'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('parseInt'), undefined, [ + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('beforeTimevalue'), + undefined, + undefined, + factory.createConditionalExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('isNaN'), + ), + undefined, + [factory.createIdentifier('beforeTimeValue')], + ), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createToken(ts.SyntaxKind.ColonToken), + factory.createIdentifier('beforeTimeValue'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createIdentifier('value'), + ]), + factory.createToken(ts.SyntaxKind.LessThanToken), + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createIdentifier('beforeTimevalue'), + ]), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createTemplateExpression( + factory.createTemplateHead( + 'The value must be before ', + 'The value must be before ', + ), + [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('strValues'), + ), + factory.createNumericLiteral('0'), + ), + factory.createTemplateTail('', ''), + ), + ], + ), + ), + ), + ], + true, + ), + ), + ]), + ]), + ), + ], + true, + ), + undefined, + ), + ), + factory.createSwitchStatement( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('type'), + ), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('Email'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('EMAIL_ADDRESS_REGEX'), + undefined, + undefined, + factory.createRegularExpressionLiteral(EscapedRegexLiterals.emailAddress), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('EMAIL_ADDRESS_REGEX'), + factory.createIdentifier('test'), + ), + undefined, + [factory.createIdentifier('value')], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral('The value must be a valid email address'), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('JSON'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('isInvalidJSON'), + undefined, + undefined, + factory.createFalse(), + ), + ], + ts.NodeFlags.Let, + ), + ), + factory.createTryStatement( + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('JSON'), + factory.createIdentifier('parse'), + ), + undefined, + [factory.createIdentifier('value')], + ), + ), + ], + true, + ), + factory.createCatchClause( + factory.createVariableDeclaration( + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createIdentifier('isInvalidJSON'), + factory.createToken(ts.SyntaxKind.EqualsToken), + factory.createTrue(), + ), + ), + ], + true, + ), + ), + undefined, + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createIdentifier('isInvalidJSON'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral('The value must be in a correct JSON format'), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('IpAddress'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('IPV_4'), + undefined, + undefined, + factory.createRegularExpressionLiteral(EscapedRegexLiterals.ipv4), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('IPV_6'), + undefined, + undefined, + factory.createRegularExpressionLiteral(EscapedRegexLiterals.ipv6), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createParenthesizedExpression( + factory.createBinaryExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('IPV_4'), + factory.createIdentifier('test'), + ), + undefined, + [factory.createIdentifier('value')], + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('IPV_6'), + factory.createIdentifier('test'), + ), + undefined, + [factory.createIdentifier('value')], + ), + ), + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral('The value must be an IPv4 or IPv6 address'), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('URL'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('isInvalidUrl'), + undefined, + undefined, + factory.createFalse(), + ), + ], + ts.NodeFlags.Let, + ), + ), + factory.createTryStatement( + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createNewExpression(factory.createIdentifier('URL'), undefined, [ + factory.createIdentifier('value'), + ]), + ), + ], + true, + ), + factory.createCatchClause( + factory.createVariableDeclaration( + factory.createIdentifier('e'), + undefined, + undefined, + undefined, + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createIdentifier('isInvalidUrl'), + factory.createToken(ts.SyntaxKind.EqualsToken), + factory.createTrue(), + ), + ), + ], + true, + ), + ), + undefined, + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createIdentifier('isInvalidUrl'), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral( + 'The value must be a valid URL that begins with a schema (i.e. http:// or mailto:)', + ), + ), + ), + ], + true, + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('Phone'), [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('PHONE'), + undefined, + undefined, + factory.createRegularExpressionLiteral(EscapedRegexLiterals.phone), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment( + factory.createIdentifier('hasError'), + factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('PHONE'), + factory.createIdentifier('test'), + ), + undefined, + [factory.createIdentifier('value')], + ), + ), + ), + factory.createPropertyAssignment( + factory.createIdentifier('errorMessage'), + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validation'), + factory.createIdentifier('validationMessage'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createStringLiteral('The value must be a valid phone number'), + ), + ), + ], + true, + ), + ), + ]), + factory.createDefaultClause([]), + ]), + ), + ], + true, + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ]; }; diff --git a/packages/codegen-ui-react/lib/utils/generate-react-hooks.ts b/packages/codegen-ui-react/lib/utils/generate-react-hooks.ts new file mode 100644 index 000000000..6d239d11e --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/generate-react-hooks.ts @@ -0,0 +1,35 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import ts, { factory, SyntaxKind } from 'typescript'; + +export const addUseEffectWrapper = (statements: ts.Statement[], dependencies: string[]) => { + return factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier('React.useEffect'), undefined, [ + factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock(statements, true), + ), + factory.createArrayLiteralExpression( + dependencies.map((dependency) => factory.createIdentifier(dependency)), + false, + ), + ]), + ); +}; diff --git a/packages/codegen-ui-react/lib/utils/json-path-fetch.ts b/packages/codegen-ui-react/lib/utils/json-path-fetch.ts new file mode 100644 index 000000000..4dc9098b5 --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/json-path-fetch.ts @@ -0,0 +1,232 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { factory, NodeFlags, SyntaxKind, VariableStatement } from 'typescript'; + +/** + * does not support array types within objects as it's currently not supported + * + * ref: https://stackoverflow.com/questions/45942118/lodash-return-array-of-values-if-the-path-is-valid + * + * @param input object input + * @param path dot notation path for the provided input + * @param accumlator array + * @returns returns value at the end of object + */ +export const fetchByPath = >(input: T, path: string, accumlator: any[] = []) => { + const currentPath = path.split('.'); + const head = currentPath.shift(); + if (input && head && input[head] !== undefined) { + if (!currentPath.length) { + accumlator.push(input[head]); + } else { + fetchByPath(input[head], currentPath.join('.'), accumlator); + } + } + return accumlator[0]; +}; + +/** + * get the generated output of the fetchByPath function in TS AST + * + * @returns VariableStatement + */ +export const getFetchByPathNodeFunction = (): VariableStatement => { + return factory.createVariableStatement( + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('fetchByPath'), + undefined, + undefined, + factory.createArrowFunction( + undefined, + [ + factory.createTypeParameterDeclaration( + factory.createIdentifier('T'), + factory.createTypeReferenceNode(factory.createIdentifier('Record'), [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.AnyKeyword), + ]), + undefined, + ), + ], + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('input'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('path'), + undefined, + undefined, + factory.createStringLiteral(''), + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('accumlator'), + undefined, + factory.createArrayTypeNode(factory.createKeywordTypeNode(SyntaxKind.AnyKeyword)), + factory.createArrayLiteralExpression([], false), + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('currentPath'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('path'), + factory.createIdentifier('split'), + ), + undefined, + [factory.createStringLiteral('.')], + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('head'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('currentPath'), + factory.createIdentifier('shift'), + ), + undefined, + [], + ), + ), + ], + NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createBinaryExpression( + factory.createIdentifier('input'), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createIdentifier('head'), + ), + factory.createToken(SyntaxKind.AmpersandAmpersandToken), + factory.createBinaryExpression( + factory.createElementAccessExpression( + factory.createIdentifier('input'), + factory.createIdentifier('head'), + ), + factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + ), + factory.createBlock( + [ + factory.createIfStatement( + factory.createPrefixUnaryExpression( + SyntaxKind.ExclamationToken, + factory.createPropertyAccessExpression( + factory.createIdentifier('currentPath'), + factory.createIdentifier('length'), + ), + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('accumlator'), + factory.createIdentifier('push'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('input'), + factory.createIdentifier('head'), + ), + ], + ), + ), + ], + true, + ), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression(factory.createIdentifier('fetchByPath'), undefined, [ + factory.createElementAccessExpression( + factory.createIdentifier('input'), + factory.createIdentifier('head'), + ), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('currentPath'), + factory.createIdentifier('join'), + ), + undefined, + [factory.createStringLiteral('.')], + ), + factory.createIdentifier('accumlator'), + ]), + ), + ], + true, + ), + ), + ], + true, + ), + undefined, + ), + factory.createReturnStatement( + factory.createElementAccessExpression( + factory.createIdentifier('accumlator'), + factory.createNumericLiteral('0'), + ), + ), + ], + true, + ), + ), + ), + ], + NodeFlags.Const, + ), + ); +}; diff --git a/packages/codegen-ui-react/lib/utils/string-formatter.ts b/packages/codegen-ui-react/lib/utils/string-formatter.ts new file mode 100644 index 000000000..3d3db3a72 --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/string-formatter.ts @@ -0,0 +1,968 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import ts, { factory } from 'typescript'; + +export const generateFormatUtil = () => [ + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('monthToShortMon'), + undefined, + factory.createTypeLiteralNode([ + factory.createIndexSignature( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('mon'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + ], + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ]), + factory.createObjectLiteralExpression( + [ + factory.createPropertyAssignment(factory.createStringLiteral('1'), factory.createStringLiteral('Jan')), + factory.createPropertyAssignment(factory.createStringLiteral('2'), factory.createStringLiteral('Feb')), + factory.createPropertyAssignment(factory.createStringLiteral('3'), factory.createStringLiteral('Mar')), + factory.createPropertyAssignment(factory.createStringLiteral('4'), factory.createStringLiteral('Apr')), + factory.createPropertyAssignment(factory.createStringLiteral('5'), factory.createStringLiteral('May')), + factory.createPropertyAssignment(factory.createStringLiteral('6'), factory.createStringLiteral('Jun')), + factory.createPropertyAssignment(factory.createStringLiteral('7'), factory.createStringLiteral('Jul')), + factory.createPropertyAssignment(factory.createStringLiteral('8'), factory.createStringLiteral('Aug')), + factory.createPropertyAssignment(factory.createStringLiteral('9'), factory.createStringLiteral('Sep')), + factory.createPropertyAssignment(factory.createStringLiteral('10'), factory.createStringLiteral('Oct')), + factory.createPropertyAssignment(factory.createStringLiteral('11'), factory.createStringLiteral('Nov')), + factory.createPropertyAssignment(factory.createStringLiteral('12'), factory.createStringLiteral('Dec')), + ], + true, + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('invalidDateStr'), + undefined, + undefined, + factory.createStringLiteral('Invalid Date'), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('DateFormatType'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('type'), + undefined, + factory.createLiteralTypeNode(factory.createStringLiteral('DateFormat')), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('format'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('DateFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('dateFormat')), + ), + ), + ]), + ), + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('DateTimeFormatType'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('type'), + undefined, + factory.createLiteralTypeNode(factory.createStringLiteral('DateTimeFormat')), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('format'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('DateTimeFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('dateTimeFormat')), + ), + ), + ]), + ), + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('TimeFormatType'), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('type'), + undefined, + factory.createLiteralTypeNode(factory.createStringLiteral('TimeFormat')), + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('format'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('TimeFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('timeFormat')), + ), + ), + ]), + ), + factory.createTypeAliasDeclaration( + undefined, + undefined, + factory.createIdentifier('FormatInputType'), + undefined, + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(factory.createIdentifier('DateFormatType'), undefined), + factory.createTypeReferenceNode(factory.createIdentifier('DateTimeFormatType'), undefined), + factory.createTypeReferenceNode(factory.createIdentifier('TimeFormatType'), undefined), + ]), + ), + factory.createFunctionDeclaration( + undefined, + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + factory.createIdentifier('formatDate'), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('date'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('dateFormat'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('DateFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('dateFormat')), + ), + undefined, + ), + ], + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createBinaryExpression( + factory.createIdentifier('date'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createBinaryExpression( + factory.createIdentifier('date'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createNull(), + ), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('date'))], true), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validDate'), + undefined, + undefined, + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Date'), + factory.createIdentifier('parse'), + ), + undefined, + [factory.createIdentifier('date')], + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validDate'), + factory.createIdentifier('toString'), + ), + undefined, + [], + ), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('invalidDateStr'), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('date'))], true), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('splitDate'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('date'), + factory.createIdentifier('split'), + ), + undefined, + [factory.createRegularExpressionLiteral(`/-|\\+|Z/`)], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('year'), + undefined, + undefined, + factory.createElementAccessExpression( + factory.createIdentifier('splitDate'), + factory.createNumericLiteral('0'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('month'), + undefined, + undefined, + factory.createElementAccessExpression( + factory.createIdentifier('splitDate'), + factory.createNumericLiteral('1'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('day'), + undefined, + undefined, + factory.createElementAccessExpression( + factory.createIdentifier('splitDate'), + factory.createNumericLiteral('2'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('truncatedMonth'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('month'), + factory.createIdentifier('replace'), + ), + undefined, + [factory.createRegularExpressionLiteral('/^0+/'), factory.createStringLiteral('')], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createSwitchStatement( + factory.createIdentifier('dateFormat'), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('locale'), [ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validDate'), + factory.createIdentifier('toLocaleDateString'), + ), + undefined, + [], + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('YYYY.MM.DD'), [ + factory.createReturnStatement( + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan(factory.createIdentifier('year'), factory.createTemplateMiddle('.', '.')), + factory.createTemplateSpan(factory.createIdentifier('month'), factory.createTemplateMiddle('.', '.')), + factory.createTemplateSpan(factory.createIdentifier('day'), factory.createTemplateTail('', '')), + ]), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('DD.MM.YYYY'), [ + factory.createReturnStatement( + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan(factory.createIdentifier('day'), factory.createTemplateMiddle('.', '.')), + factory.createTemplateSpan(factory.createIdentifier('month'), factory.createTemplateMiddle('.', '.')), + factory.createTemplateSpan(factory.createIdentifier('year'), factory.createTemplateTail('', '')), + ]), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('MM/DD/YYYY'), [ + factory.createReturnStatement( + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan(factory.createIdentifier('month'), factory.createTemplateMiddle('/', '/')), + factory.createTemplateSpan(factory.createIdentifier('day'), factory.createTemplateMiddle('/', '/')), + factory.createTemplateSpan(factory.createIdentifier('year'), factory.createTemplateTail('', '')), + ]), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('Mmm DD, YYYY'), [ + factory.createReturnStatement( + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan( + factory.createElementAccessExpression( + factory.createIdentifier('monthToShortMon'), + factory.createIdentifier('truncatedMonth'), + ), + factory.createTemplateMiddle(' ', ' '), + ), + factory.createTemplateSpan(factory.createIdentifier('day'), factory.createTemplateMiddle(', ', ', ')), + factory.createTemplateSpan(factory.createIdentifier('year'), factory.createTemplateTail('', '')), + ]), + ), + ]), + factory.createDefaultClause([factory.createReturnStatement(factory.createIdentifier('date'))]), + ]), + ), + ], + true, + ), + ), + factory.createFunctionDeclaration( + undefined, + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + factory.createIdentifier('formatTime'), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('time'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('timeFormat'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('TimeFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('timeFormat')), + ), + undefined, + ), + ], + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createBinaryExpression( + factory.createIdentifier('time'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createBinaryExpression( + factory.createIdentifier('time'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createNull(), + ), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('time'))], true), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('splitTime'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('time'), + factory.createIdentifier('split'), + ), + undefined, + [factory.createRegularExpressionLiteral('/:|Z/')], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('splitTime'), + factory.createIdentifier('length'), + ), + factory.createToken(ts.SyntaxKind.LessThanToken), + factory.createNumericLiteral('3'), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('time'))], true), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('validTime'), + undefined, + undefined, + factory.createNewExpression(factory.createIdentifier('Date'), undefined, []), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('setHours'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('parseInt'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('splitTime'), + factory.createNumericLiteral('0'), + ), + factory.createNumericLiteral('10'), + ], + ), + ], + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('setMinutes'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('parseInt'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('splitTime'), + factory.createNumericLiteral('1'), + ), + factory.createNumericLiteral('10'), + ], + ), + ], + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('splitSeconds'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createElementAccessExpression( + factory.createIdentifier('splitTime'), + factory.createNumericLiteral('2'), + ), + factory.createIdentifier('split'), + ), + undefined, + [factory.createStringLiteral('.')], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('setSeconds'), + ), + undefined, + [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('parseInt'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('splitSeconds'), + factory.createNumericLiteral('0'), + ), + factory.createNumericLiteral('10'), + ], + ), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('parseInt'), + ), + undefined, + [ + factory.createElementAccessExpression( + factory.createIdentifier('splitSeconds'), + factory.createNumericLiteral('1'), + ), + factory.createNumericLiteral('10'), + ], + ), + ], + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('toString'), + ), + undefined, + [], + ), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('invalidDateStr'), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('time'))], true), + undefined, + ), + factory.createSwitchStatement( + factory.createIdentifier('timeFormat'), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('locale'), [ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('toLocaleTimeString'), + ), + undefined, + [], + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('hours24'), [ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('toLocaleTimeString'), + ), + undefined, + [factory.createStringLiteral('en-gb')], + ), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('hours12'), [ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('validTime'), + factory.createIdentifier('toLocaleTimeString'), + ), + undefined, + [factory.createStringLiteral('en-us')], + ), + ), + ]), + factory.createDefaultClause([factory.createReturnStatement(factory.createIdentifier('time'))]), + ]), + ), + ], + true, + ), + ), + factory.createFunctionDeclaration( + undefined, + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + factory.createIdentifier('formatDateTime'), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('dateTimeStr'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('dateTimeFormat'), + undefined, + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(factory.createIdentifier('DateTimeFormat'), undefined), + factory.createLiteralTypeNode(factory.createStringLiteral('dateTimeFormat')), + ), + undefined, + ), + ], + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + factory.createBlock( + [ + factory.createIfStatement( + factory.createBinaryExpression( + factory.createBinaryExpression( + factory.createIdentifier('dateTimeStr'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(ts.SyntaxKind.BarBarToken), + factory.createBinaryExpression( + factory.createIdentifier('dateTimeStr'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createNull(), + ), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('dateTimeStr'))], true), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('dateTime'), + undefined, + undefined, + factory.createConditionalExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createRegularExpressionLiteral('/^d+$/'), + factory.createIdentifier('test'), + ), + undefined, + [factory.createIdentifier('dateTimeStr')], + ), + factory.createToken(ts.SyntaxKind.QuestionToken), + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Number'), + factory.createIdentifier('parseInt'), + ), + undefined, + [factory.createIdentifier('dateTimeStr'), factory.createNumericLiteral('10')], + ), + ]), + factory.createToken(ts.SyntaxKind.ColonToken), + factory.createNewExpression(factory.createIdentifier('Date'), undefined, [ + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('Date'), + factory.createIdentifier('parse'), + ), + undefined, + [factory.createIdentifier('dateTimeStr')], + ), + ]), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('dateTime'), + factory.createIdentifier('toString'), + ), + undefined, + [], + ), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createIdentifier('invalidDateStr'), + ), + factory.createBlock([factory.createReturnStatement(factory.createIdentifier('dateTimeStr'))], true), + undefined, + ), + factory.createIfStatement( + factory.createBinaryExpression( + factory.createIdentifier('dateTimeFormat'), + factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + factory.createStringLiteral('locale'), + ), + factory.createBlock( + [ + factory.createReturnStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('dateTime'), + factory.createIdentifier('toLocaleString'), + ), + undefined, + [], + ), + ), + ], + true, + ), + undefined, + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('dateAndTime'), + undefined, + undefined, + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('dateTime'), + factory.createIdentifier('toISOString'), + ), + undefined, + [], + ), + factory.createIdentifier('split'), + ), + undefined, + [factory.createStringLiteral('T')], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('date'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('formatDate'), undefined, [ + factory.createElementAccessExpression( + factory.createIdentifier('dateAndTime'), + factory.createNumericLiteral('0'), + ), + factory.createPropertyAccessExpression( + factory.createIdentifier('dateTimeFormat'), + factory.createIdentifier('dateFormat'), + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('time'), + undefined, + undefined, + factory.createCallExpression(factory.createIdentifier('formatTime'), undefined, [ + factory.createElementAccessExpression( + factory.createIdentifier('dateAndTime'), + factory.createNumericLiteral('1'), + ), + factory.createPropertyAccessExpression( + factory.createIdentifier('dateTimeFormat'), + factory.createIdentifier('timeFormat'), + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createReturnStatement( + factory.createTemplateExpression(factory.createTemplateHead('', ''), [ + factory.createTemplateSpan(factory.createIdentifier('date'), factory.createTemplateMiddle(' - ', ' - ')), + factory.createTemplateSpan(factory.createIdentifier('time'), factory.createTemplateTail('', '')), + ]), + ), + ], + true, + ), + ), + factory.createFunctionDeclaration( + undefined, + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + factory.createIdentifier('formatter'), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('value'), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('formatterInput'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('FormatInputType'), undefined), + undefined, + ), + ], + undefined, + factory.createBlock( + [ + factory.createSwitchStatement( + factory.createPropertyAccessExpression( + factory.createIdentifier('formatterInput'), + factory.createIdentifier('type'), + ), + factory.createCaseBlock([ + factory.createCaseClause(factory.createStringLiteral('DateFormat'), [ + factory.createReturnStatement( + factory.createCallExpression(factory.createIdentifier('formatDate'), undefined, [ + factory.createIdentifier('value'), + factory.createPropertyAccessExpression( + factory.createIdentifier('formatterInput'), + factory.createIdentifier('format'), + ), + ]), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('DateTimeFormat'), [ + factory.createReturnStatement( + factory.createCallExpression(factory.createIdentifier('formatDateTime'), undefined, [ + factory.createIdentifier('value'), + factory.createPropertyAccessExpression( + factory.createIdentifier('formatterInput'), + factory.createIdentifier('format'), + ), + ]), + ), + ]), + factory.createCaseClause(factory.createStringLiteral('TimeFormat'), [ + factory.createReturnStatement( + factory.createCallExpression(factory.createIdentifier('formatTime'), undefined, [ + factory.createIdentifier('value'), + factory.createPropertyAccessExpression( + factory.createIdentifier('formatterInput'), + factory.createIdentifier('format'), + ), + ]), + ), + ]), + factory.createDefaultClause([factory.createReturnStatement(factory.createIdentifier('value'))]), + ]), + ), + ], + true, + ), + ), +]; diff --git a/packages/codegen-ui-react/lib/views/react-view-renderer.ts b/packages/codegen-ui-react/lib/views/react-view-renderer.ts new file mode 100644 index 000000000..7b1a482f0 --- /dev/null +++ b/packages/codegen-ui-react/lib/views/react-view-renderer.ts @@ -0,0 +1,479 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + GenericDataSchema, + StudioNode, + StudioView, + StudioTemplateRenderer, + TableDefinition, + generateTableDefinition, + ViewMetadata, + handleCodegenErrors, + validateViewSchema, + StudioComponentPredicate, +} from '@aws-amplify/codegen-ui'; +import { + addSyntheticLeadingComment, + BindingElement, + EmitHint, + factory, + FunctionDeclaration, + JsxElement, + JsxFragment, + JsxSelfClosingElement, + Modifier, + NodeFlags, + ObjectLiteralExpression, + ScriptKind, + Statement, + SyntaxKind, + TypeAliasDeclaration, +} from 'typescript'; +import { EOL } from 'os'; +import { + buildBaseCollectionVariableStatement, + buildPrinter, + buildSortFunction, + defaultRenderConfig, + getDeclarationFilename, + transpile, +} from '../react-studio-template-renderer-helper'; +import { ImportCollection, ImportSource, ImportValue } from '../imports'; +import { Primitive, PrimitiveTypeParameter } from '../primitive'; +import { getComponentPropName } from '../react-component-render-helper'; +import { ReactOutputManager } from '../react-output-manager'; +import { ReactRenderConfig, scriptKindToFileExtension } from '../react-render-config'; +import { RequiredKeys } from '../utils/type-utils'; +import { + buildDataStoreCollectionCall, + getFilterName, + getPaginationName, + getPredicateName, + needsFormatter, +} from '../react-table-renderer-helper'; + +export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< + string, + StudioView, + ReactOutputManager, + { + componentText: string; + renderComponentToFilesystem: (outputPath: string) => Promise; + } +> { + protected importCollection = new ImportCollection(); + + protected renderConfig: RequiredKeys; + + protected viewDefinition: TableDefinition; + + protected viewComponent: StudioView; + + protected viewMetadata: ViewMetadata; + + public fileName: string; + + abstract renderJsx(view: StudioView, parent?: StudioNode): JsxElement | JsxFragment | JsxSelfClosingElement; + + constructor(component: StudioView, dataSchema: GenericDataSchema | undefined, renderConfig: ReactRenderConfig) { + super(component, new ReactOutputManager(), renderConfig); + this.renderConfig = { + ...defaultRenderConfig, + ...renderConfig, + }; + // the super class creates a component aka form which is what we pass in this extended implmentation + this.fileName = `${this.component.name}.${scriptKindToFileExtension(this.renderConfig.script)}`; + + switch (component.viewConfiguration.type) { + case 'Table': + this.viewDefinition = generateTableDefinition(component, dataSchema); + break; + default: + throw new Error(`Type: ${component.viewConfiguration.type} is not supported.`); + } + + this.viewComponent = component; + + // find if formatter is required + if (needsFormatter(component.viewConfiguration)) { + this.importCollection.addMappedImport(ImportValue.FORMATTER); + } + + this.viewMetadata = { + id: component.id, + name: component.name, + fieldFormatting: {}, + }; + } + + @handleCodegenErrors + renderComponentOnly() { + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.viewComponent); + const requiredDataModels = []; + + const imports = this.importCollection.buildImportStatements(); + + let importsText = ''; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + importsText += result + EOL; + }); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, false); + + const result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + + // do not produce declaration becuase it is not used + const { componentText: compText } = transpile(result, { ...this.renderConfig, renderTypeDeclarations: false }); + + const { type, model } = this.viewComponent.dataSource; + if (type === 'DataStore' && model) { + requiredDataModels.push(model); + // TODO: require other models if form is handling querying relational models + } + + return { compText, importsText, requiredDataModels }; + } + + @handleCodegenErrors + renderComponentInternal() { + const { printer, file } = buildPrinter(this.fileName, this.renderConfig); + + const variableStatements = this.buildVariableStatements(); + const jsx = this.renderJsx(this.viewComponent); + + const wrappedFunction = this.renderFunctionWrapper(this.component.name, variableStatements, jsx, true); + const propsDeclaration = this.renderBindingPropsType(); + + const imports = this.importCollection.buildImportStatements(); + + let componentText = `/* eslint-disable */${EOL}`; + + imports.forEach((importStatement) => { + const result = printer.printNode(EmitHint.Unspecified, importStatement, file); + componentText += result + EOL; + }); + + componentText += EOL; + + propsDeclaration.forEach((typeNode) => { + const propsPrinted = printer.printNode(EmitHint.Unspecified, typeNode, file); + componentText += propsPrinted; + }); + + const result = printer.printNode(EmitHint.Unspecified, wrappedFunction, file); + componentText += result; + + const { componentText: transpiledComponentText, declaration } = transpile(componentText, this.renderConfig); + + return { + componentText: transpiledComponentText, + declaration, + renderComponentToFilesystem: async (outputPath: string) => { + await this.renderComponentToFilesystem(transpiledComponentText)(this.fileName)(outputPath); + if (declaration) { + await this.renderComponentToFilesystem(declaration)(getDeclarationFilename(this.fileName))(outputPath); + } + }, + }; + } + + renderFunctionWrapper( + componentName: string, + variableStatements: Statement[], + jsx: JsxElement | JsxFragment | JsxSelfClosingElement, + renderExport: boolean, + ): FunctionDeclaration { + const componentPropType = getComponentPropName(componentName); + const jsxStatement = factory.createReturnStatement( + factory.createParenthesizedExpression( + this.renderConfig.script !== ScriptKind.TSX + ? jsx + : /* add ts-ignore comment above jsx statement. Generated props are incompatible with amplify-ui props */ + addSyntheticLeadingComment( + factory.createParenthesizedExpression(jsx), + SyntaxKind.MultiLineCommentTrivia, + ' @ts-ignore: TS2322 ', + true, + ), + ), + ); + const codeBlockContent = variableStatements.concat([jsxStatement]); + const modifiers: Modifier[] = renderExport + ? [factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.DefaultKeyword)] + : []; + const typeParameter = PrimitiveTypeParameter[Primitive[this.viewComponent?.viewConfiguration.type as Primitive]]; + // only use type parameter reference if one was declared + const typeParameterReference = typeParameter && typeParameter.declaration() ? typeParameter.reference() : undefined; + return factory.createFunctionDeclaration( + undefined, + modifiers, + undefined, + factory.createIdentifier(componentName), + typeParameter ? typeParameter.declaration() : undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'props', + undefined, + factory.createTypeReferenceNode(componentPropType, typeParameterReference), + undefined, + ), + ], + factory.createTypeReferenceNode( + factory.createQualifiedName(factory.createIdentifier('React'), factory.createIdentifier('ReactElement')), + undefined, + ), + factory.createBlock(codeBlockContent, true), + ); + } + + buildVariableStatements() { + const statements: Statement[] = []; + const elements: BindingElement[] = []; + const { type, model, predicate, sort } = this.viewComponent.dataSource; + const isDataStoreEnabled = type === 'DataStore' && model; + if (isDataStoreEnabled) { + this.importCollection.addImport(ImportSource.LOCAL_MODELS, this.component.dataSource.type); + this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); + elements.push( + factory.createBindingElement( + undefined, + factory.createIdentifier('items'), + factory.createIdentifier('itemsProps'), + undefined, + ), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('predicateOverride'), undefined), + ); + } else { + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('items'), undefined)); + } + + // add base Props + + // props + const props = [ + factory.createBindingElement(undefined, undefined, factory.createIdentifier('formatOverride'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('highlightOnHover'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('onRowClick'), undefined), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('disableHeaders'), undefined), + ]; + elements.push(...props); + + // get rest of props to pass to top level component + elements.push( + factory.createBindingElement( + factory.createToken(SyntaxKind.DotDotDotToken), + undefined, + factory.createIdentifier('rest'), + undefined, + ), + ); + + // add binding elments to statements + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createObjectBindingPattern(elements), + undefined, + undefined, + factory.createIdentifier('props'), + ), + ], + NodeFlags.Const, + ), + ), + ); + + if (isDataStoreEnabled) { + /** + * builds predicate variable + */ + if (predicate) { + this.importCollection.addMappedImport(ImportValue.CREATE_DATA_STORE_PREDICATE); + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getFilterName(model), + undefined, + undefined, + this.predicateToObjectLiteralExpression(predicate), + ), + ], + NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getPredicateName(model), + undefined, + undefined, + factory.createCallExpression( + factory.createIdentifier('createDataStorePredicate'), + [factory.createTypeReferenceNode(factory.createIdentifier(model), undefined)], + [factory.createIdentifier(getFilterName(model))], + ), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + /** + * builds sort function + */ + if (sort) { + this.importCollection.addMappedImport(ImportValue.SORT_DIRECTION); + this.importCollection.addMappedImport(ImportValue.SORT_PREDICATE); + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getPaginationName(model), + undefined, + undefined, + factory.createObjectLiteralExpression([ + factory.createPropertyAssignment(factory.createIdentifier('sort'), buildSortFunction(model, sort)), + ]), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + /* + if datastore enabled + const myViewDataStore = useDataStoreBinding({ + model: Model, + type: 'Collection' + }).items; + const items = itemsProp !== undefined ? itemsProp : myViewDataStore; + + if custom enabled + uses regular items array for formatting + */ + const dsItemsName = factory.createIdentifier(`${this.viewComponent.name}DataStore`); + statements.push( + buildBaseCollectionVariableStatement( + dsItemsName, + buildDataStoreCollectionCall( + model, + predicate ? getPredicateName(model) : undefined, + sort ? getPaginationName(model) : undefined, + ), + ), + // checks to see if an override was passed + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('items'), + undefined, + undefined, + factory.createConditionalExpression( + factory.createBinaryExpression( + factory.createIdentifier('itemsProp'), + factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(SyntaxKind.QuestionToken), + factory.createIdentifier('itemsProp'), + factory.createToken(SyntaxKind.ColonToken), + dsItemsName, + ), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + return statements; + } + + private predicateToObjectLiteralExpression(predicate: StudioComponentPredicate): ObjectLiteralExpression { + return factory.createObjectLiteralExpression( + Object.entries(predicate).map(([key, value]) => { + return factory.createPropertyAssignment( + factory.createIdentifier(key), + key === 'and' || key === 'or' + ? factory.createArrayLiteralExpression( + (value as StudioComponentPredicate[]).map( + (pred: StudioComponentPredicate) => this.predicateToObjectLiteralExpression(pred), + false, + ), + ) + : factory.createStringLiteral(value as string), + ); + }, false), + ); + } + + private renderBindingPropsType(): TypeAliasDeclaration[] { + const escapeHatchTypeNode = factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('overrides'), + factory.createToken(SyntaxKind.QuestionToken), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(factory.createIdentifier('EscapeHatchProps'), undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + factory.createLiteralTypeNode(factory.createNull()), + ]), + ), + ]); + const formPropType = getComponentPropName(this.component.name); + + this.importCollection.addMappedImport(ImportValue.ESCAPE_HATCH_PROPS); + + return [ + factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier(formPropType), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('React.PropsWithChildren'), [ + factory.createIntersectionTypeNode([escapeHatchTypeNode]), + ]), + ), + ]; + } + + validateSchema(component: StudioView): void { + validateViewSchema(component); + } +} diff --git a/packages/codegen-ui-react/package-lock.json b/packages/codegen-ui-react/package-lock.json index 65b5b70ea..a8ddb0a2f 100644 --- a/packages/codegen-ui-react/package-lock.json +++ b/packages/codegen-ui-react/package-lock.json @@ -6,11 +6,11 @@ "packages": { "": { "name": "@aws-amplify/codegen-ui-react", - "version": "2.3.1", + "version": "2.3.2", "license": "Apache-2.0", "dependencies": { "@typescript/vfs": "~1.3.5", - "typescript": "~4.4.4" + "typescript": "<=4.5.0" }, "devDependencies": { "@aws-amplify/ui-react": "^2.1.0", diff --git a/packages/codegen-ui-react/package.json b/packages/codegen-ui-react/package.json index d9b4c0aa8..1205a066a 100644 --- a/packages/codegen-ui-react/package.json +++ b/packages/codegen-ui-react/package.json @@ -29,7 +29,7 @@ "dependencies": { "@aws-amplify/codegen-ui": "2.3.2", "@typescript/vfs": "~1.3.5", - "typescript": "^4.4.4" + "typescript": "<=4.5.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", diff --git a/packages/codegen-ui/CHANGELOG.md b/packages/codegen-ui/CHANGELOG.md index 2026c7d16..2939dfa9e 100644 --- a/packages/codegen-ui/CHANGELOG.md +++ b/packages/codegen-ui/CHANGELOG.md @@ -5,38 +5,23 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## [2.3.2](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.1...v2.3.2) (2022-07-22) - ### Bug Fixes -* limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) - - - - +- limit workers during testing in ci ([#531](https://github.com/aws-amplify/amplify-codegen-ui/issues/531)) ([be36527](https://github.com/aws-amplify/amplify-codegen-ui/commit/be36527e86e76360e3368daa62ece4f9616bd69d)) ## [2.3.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.0...v2.3.1) (2022-07-15) **Note:** Version bump only for package @aws-amplify/codegen-ui - - - - # [2.3.0](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.1...v2.3.0) (2022-07-14) - ### Bug Fixes -* handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) - +- handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) ### Features -* adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) - - - - +- adding breakpoint functionality in theme generation ([#515](https://github.com/aws-amplify/amplify-codegen-ui/issues/515)) ([28f97aa](https://github.com/aws-amplify/amplify-codegen-ui/commit/28f97aa7a290e3fd25efc6f0d51a39403d79b947)) ## [2.2.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.0...v2.2.1) (2022-06-15) diff --git a/packages/codegen-ui/example-schemas/componentWithBreakpoint.json b/packages/codegen-ui/example-schemas/componentWithBreakpoint.json new file mode 100644 index 000000000..40f0d8c7d --- /dev/null +++ b/packages/codegen-ui/example-schemas/componentWithBreakpoint.json @@ -0,0 +1,33 @@ +{ + "id": "1234-5678-9010", + "componentType": "Button", + "name": "ComponentWithBreakpoint", + "properties": { + "children": { + "value": "ComponentWithBreakpoint" + } + }, + "variants": [ + { + "variantValues": { + "breakpoint": "small" + }, + "overrides": { + "ComponentWithVariant": { + "fontSize": "12px" + } + } + }, + { + "variantValues": { + "breakpoint": "medium" + }, + "overrides": { + "ComponentWithVariant": { + "fontSize": "40px" + } + } + } + ], + "schemaVersion": "1.0" +} diff --git a/packages/codegen-ui/example-schemas/datastore/input-gallery.json b/packages/codegen-ui/example-schemas/datastore/input-gallery.json new file mode 100644 index 000000000..568555922 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/input-gallery.json @@ -0,0 +1,123 @@ +{ + "models": { + "InputGallery": { + "name": "InputGallery", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "num": { + "name": "num", + "isArray": false, + "type": "Int", + "isRequired": false, + "attributes": [] + }, + "rootbeer": { + "name": "rootbeer", + "isArray": false, + "type": "Float", + "isRequired": false, + "attributes": [] + }, + "attend": { + "name": "attend", + "isArray": false, + "type": "Boolean", + "isRequired": false, + "attributes": [] + }, + "maybeSlide": { + "name": "maybeSlide", + "isArray": false, + "type": "Boolean", + "isRequired": false, + "attributes": [] + }, + "maybeCheck": { + "name": "maybeCheck", + "isArray": false, + "type": "Boolean", + "isRequired": false, + "attributes": [] + }, + "arrayTypeField": { + "name": "arrayTypeField", + "isArray": true, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "timestamp": { + "name": "timestamp", + "isArray": false, + "type": "AWSTimestamp", + "isRequired": false, + "attributes": [] + }, + "ippy": { + "name": "ippy", + "isArray": false, + "type": "AWSIPAddress", + "isRequired": false, + "attributes": [] + }, + "timeisnow": { + "name": "timeisnow", + "isArray": false, + "type": "AWSTime", + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "InputGalleries", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "private", + "provider": "iam", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ] + } + }, + "enums": {}, + "nonModels": {}, + "version": "000000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/datastore/person.json b/packages/codegen-ui/example-schemas/datastore/person.json new file mode 100644 index 000000000..238b2ff57 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/person.json @@ -0,0 +1,48 @@ +{ + "dataSourceType": "DataStore", + "models": { + "Person": { + "fields": { + "id": { + "dataType": "ID", + "required": true, + "readOnly": false, + "isArray": false + }, + "name": { + "dataType": "String", + "required": false, + "readOnly": false, + "isArray": false + }, + "hireDate": { + "dataType": "AWSDateTime", + "required": false, + "readOnly": false, + "isArray": false + }, + "comments": { + "dataType": "String", + "required": false, + "readOnly": false, + "isArray": false + }, + "createdAt": { + "dataType": "AWSDateTime", + "required": false, + "readOnly": true, + "isArray": false + }, + "updatedAt": { + "dataType": "AWSDateTime", + "required": false, + "readOnly": true, + "isArray": false + } + } + } + }, + "enums": {}, + "nonModels": {}, + "version": "000000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/datastore/post-ds.json b/packages/codegen-ui/example-schemas/datastore/post-ds.json new file mode 100644 index 000000000..61e902db5 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/post-ds.json @@ -0,0 +1,106 @@ +{ + "models": { + "Post": { + "name": "Post", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "caption": { + "name": "caption", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "username": { + "name": "username", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "post_url": { + "name": "post_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "profile_url": { + "name": "profile_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "status": { + "name": "status", + "isArray": false, + "type": { + "enum": "PostStatus" + }, + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Posts", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "private", + "provider": "iam", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ] + } + }, + "enums": { + "PostStatus": { + "name": "PostStatus", + "values": [ + "PENDING", + "POSTED", + "IN_REVIEW" + ] + } + }, + "nonModels": {}, + "version": "00000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/datastore/post.json b/packages/codegen-ui/example-schemas/datastore/post.json new file mode 100644 index 000000000..000658e49 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/post.json @@ -0,0 +1,89 @@ +{ + "models": { + "Post": { + "name": "Post", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "caption": { + "name": "caption", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "username": { + "name": "username", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "post_url": { + "name": "post_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "profile_url": { + "name": "profile_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Posts", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "private", + "provider": "iam", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ] + } + }, + "enums": { + }, + "nonModels": {}, + "version": "000000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/bio-nested-create.json b/packages/codegen-ui/example-schemas/forms/bio-nested-create.json new file mode 100644 index 000000000..3de030251 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/bio-nested-create.json @@ -0,0 +1,63 @@ +{ + "cta": {}, + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "JSON" + }, + "fields": { + "firstName": { + "inputType": { + "type": "TextField" + }, + "label": "firstName", + "position": { + "fixed": "first" + } + }, + "lastName": { + "inputType": { + "type": "TextField" + }, + "label": "lastName", + "position": { + "below": "firstName" + } + }, + "bio.favoriteQuote": { + "inputType": { + "type": "TextField" + }, + "label": "favoriteQuote", + "position": { + "below": "bio" + } + }, + "bio.favoriteAnimal": { + "inputType": { + "type": "TextField" + }, + "label": "favoriteAnimal", + "position": { + "below": "bio.favoriteQuote" + } + } + }, + "formActionType": "create", + "name": "NestedJson", + "sectionalElements": { + "bio": { + "level": 3, + "position": { + "below": "lastName" + }, + "text": "bio", + "type": "Heading" + } + }, + "style": { + "horizontalGap": { + "tokenReference": "space.large" + } + }, + "id": "f-BD6Fl4FX8B6XL7Il9a" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/bio-nested-update.json b/packages/codegen-ui/example-schemas/forms/bio-nested-update.json new file mode 100644 index 000000000..f64975c91 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/bio-nested-update.json @@ -0,0 +1,59 @@ +{ + "cta" : { }, + "dataType" : { + "dataSourceType" : "Custom", + "dataTypeName" : "JSON" + }, + "fields" : { + "firstName" : { + "inputType" : { + "type" : "TextField" + }, + "label" : "firstName", + "position" : { + "fixed" : "first" + } + }, + "lastName" : { + "inputType" : { + "type" : "TextField" + }, + "label" : "lastName", + "position" : { + "below" : "firstName" + } + }, + "bio.favoriteQuote" : { + "inputType" : { + "type" : "TextField" + }, + "label" : "favoriteQuote", + "position" : { + "below" : "bio" + } + }, + "bio.favoriteAnimal" : { + "inputType" : { + "type" : "TextField" + }, + "label" : "favoriteAnimal", + "position" : { + "below" : "bio.favoriteQuote" + } + } + }, + "formActionType" : "update", + "name" : "NestedJson", + "sectionalElements" : { + "bio" : { + "level" : 3, + "position" : { + "below" : "lastName" + }, + "text" : "bio", + "type" : "Heading" + } + }, + "style" : { }, + "id" : "f-BD6Fl4FX8B6XL7Il9a" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/custom-with-sectional-elements.json b/packages/codegen-ui/example-schemas/forms/custom-with-sectional-elements.json new file mode 100644 index 000000000..c5f0f7ce2 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/custom-with-sectional-elements.json @@ -0,0 +1,44 @@ +{ + "name": "CustomWithSectionalElements", + "formActionType": "create", + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "Post" + }, + "fields": { + "name": { + "inputType": { + "type": "TextField" + } + } + }, + "sectionalElements": { + "myHeading": { + "position": { + "fixed": "first" + }, + "type": "Heading", + "level": 2, + "text": "Create a Post" + }, + "myText": { + "position": { + "below": "name" + }, + "type": "Text", + "text": "Did you put your name above?" + }, + "myDivider": { + "position": { + "below": "myText" + }, + "type": "Divider" + } + }, + "style": {}, + "cta": { + "clear": {}, + "cancel": {}, + "submit": {} + } + } \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/input-gallery-create.json b/packages/codegen-ui/example-schemas/forms/input-gallery-create.json new file mode 100644 index 000000000..f431d2909 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/input-gallery-create.json @@ -0,0 +1,35 @@ +{ + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "InputGallery" + }, + "fields": { + "attend": { + "inputType": { + "required": "false", + "type": "RadioGroupField" + } + }, + "maybeSlide": { + "inputType": { + "type": "ToggleButton" + } + }, + "maybeCheck": { + "inputType": { + "type": "CheckboxField" + } + } + }, + "formActionType": "create", + "name": "InputGalleryCreateForm", + "schemaVersion": "1.0", + "sectionalElements": {}, + "style": {}, + "cta": { + "clear": {}, + "cancel": {}, + "submit": {} + }, + "id": "00001" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/input-gallery-update.json b/packages/codegen-ui/example-schemas/forms/input-gallery-update.json new file mode 100644 index 000000000..173daec1e --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/input-gallery-update.json @@ -0,0 +1,35 @@ +{ + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "InputGallery" + }, + "fields": { + "attend": { + "inputType": { + "required": "false", + "type": "RadioGroupField" + } + }, + "maybeSlide": { + "inputType": { + "type": "ToggleButton" + } + }, + "maybeCheck": { + "inputType": { + "type": "CheckboxField" + } + } + }, + "formActionType": "update", + "name": "InputGalleryCreateForm", + "schemaVersion": "1.0", + "sectionalElements": {}, + "style": {}, + "cta": { + "clear": {}, + "cancel": {}, + "submit": {} + }, + "id": "00001" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-custom-create.json b/packages/codegen-ui/example-schemas/forms/post-custom-create.json new file mode 100644 index 000000000..cbdd13d59 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-custom-create.json @@ -0,0 +1,66 @@ +{ + "name": "CustomDataForm", + "formActionType": "create", + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "Post" + }, + "fields": { + "name": { + "inputType": { + "required": true, + "type": "TextField", + "name": "name", + "defaultValue": "John Doe" + }, + "label": "name" + }, + "email": { + "inputType": { + "required": true, + "type": "TextField", + "name": "email", + "defaultValue": "johndoe@amplify.com" + }, + "label": "E-mail" + }, + "city": { + "inputType": { + "type": "SelectField", + "defaultValue": "New York", + "valueMappings": { + "bindingProperties": {}, + "values": [{"value": {"value": "Los Angeles"}}, {"value": {"value": "Houston"}}, {"value": {"value": "New York"}}] + } + } + }, + "category": { + "inputType": { + "type": "RadioGroupField", + "defaultValue": "Hobbies", + "valueMappings": { + "bindingProperties": {}, + "values": [{"value": {"value": "Hobbies"}}, {"value": {"value": "Travel"}}, {"value": {"value": "Health"}}] + } + } + }, + "pages": { + "inputType": { + "type": "StepperField" + } + } + }, + "sectionalElements": {}, + "style": {}, + "cta": { + "clear": { + "children": "empty" + }, + "cancel": { + "children": "go back" + }, + "submit": { + "children": "create" + } + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-custom-update.json b/packages/codegen-ui/example-schemas/forms/post-custom-update.json new file mode 100644 index 000000000..12453cf56 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-custom-update.json @@ -0,0 +1,66 @@ +{ + "name": "CustomDataForm", + "formActionType": "update", + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "Post" + }, + "fields": { + "name": { + "inputType": { + "required": true, + "type": "TextField", + "name": "name", + "defaultValue": "John Doe" + }, + "label": "name" + }, + "email": { + "inputType": { + "required": true, + "type": "TextField", + "name": "email", + "defaultValue": "johndoe@amplify.com" + }, + "label": "E-mail" + }, + "city": { + "inputType": { + "type": "SelectField", + "defaultValue": "New York", + "valueMappings": { + "bindingProperties": {}, + "values": [{"value": {"value": "Los Angeles"}}, {"value": {"value": "Houston"}}, {"value": {"value": "New York"}}] + } + } + }, + "category": { + "inputType": { + "type": "RadioGroupField", + "defaultValue": "Hobbies", + "valueMappings": { + "bindingProperties": {}, + "values": [{"value": {"value": "Hobbies"}}, {"value": {"value": "Travel"}}, {"value": {"value": "Health"}}] + } + } + }, + "pages": { + "inputType": { + "type": "StepperField" + } + } + }, + "sectionalElements": {}, + "style": {}, + "cta": { + "clear": { + "children": "empty" + }, + "cancel": { + "children": "go back" + }, + "submit": { + "children": "create" + } + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-datastore-create-row.json b/packages/codegen-ui/example-schemas/forms/post-datastore-create-row.json new file mode 100644 index 000000000..8f8472e7d --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-datastore-create-row.json @@ -0,0 +1,77 @@ +{ + "cta" : { + "cancel" : { }, + "clear" : { }, + "position" : "bottom", + "submit" : { } + }, + "dataType" : { + "dataSourceType" : "DataStore", + "dataTypeName" : "Post" + }, + "fields" : { + "caption" : { + "inputType" : { + "placeholder" : "i love code", + "required" : false, + "type" : "TextField" + }, + "label" : "Caption", + "position" : { + "rightOf" : "username" + } + }, + "username" : { + "inputType" : { + "placeholder" : "john", + "required" : false, + "type" : "TextField" + }, + "label" : "Username", + "position" : { + "fixed" : "first" + }, + "validations" : [ { + "numValues" : [ 2 ], + "type" : "GreaterThanChar", + "validationMessage" : "needs to be of length 2" + } ] + }, + "post_url" : { + "inputType" : { + "descriptiveText" : "post url to use for the component", + "required" : false, + "type" : "URLField" + }, + "label" : "Post url", + "position" : { + "below" : "username" + } + }, + "profile_url" : { + "inputType" : { + "descriptiveText" : "profile image url", + "required" : false, + "type" : "URLField" + }, + "label" : "Profile url", + "position" : { + "below" : "post_url" + } + }, + "status" : { + "inputType": { + "type": "SelectField" + }, + "position" : { + "below" : "profile_url" + } + } + }, + "formActionType" : "create", + "name" : "PostCreateFormRow", + "schemaVersion" : "1.0", + "sectionalElements" : { }, + "style" : { }, + "id" : "f-000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-datastore-create.json b/packages/codegen-ui/example-schemas/forms/post-datastore-create.json new file mode 100644 index 000000000..126cecfdd --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-datastore-create.json @@ -0,0 +1,17 @@ +{ + "name": "MyPostForm", + "formActionType": "create", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "Post" + }, + "fields": {}, + "sectionalElements": {}, + "style": {}, + "cta": { + "position": "top", + "clear": {}, + "cancel": {}, + "submit": {} + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/forms/post-datastore-update.json b/packages/codegen-ui/example-schemas/forms/post-datastore-update.json new file mode 100644 index 000000000..6990edd94 --- /dev/null +++ b/packages/codegen-ui/example-schemas/forms/post-datastore-update.json @@ -0,0 +1,47 @@ +{ + "id": "123", + "name": "MyPostForm", + "formActionType": "update", + "dataType": { + "dataSourceType": "DataStore", + "dataTypeName": "Post" + }, + "fields": { + "TextAreaFieldbbd63464": { + "inputType": { + "type": "TextAreaField" + }, + "position": { + "fixed": "first" + } + }, + "caption": { + "position": { + "below": "TextAreaFieldbbd63464" + } + }, + "username": { + "position": { + "below": "caption" + } + }, + "post_url": { + "position": { + "below": "username" + } + }, + "profile_url": { + "position": { + "below": "username" + } + } + }, + "sectionalElements": {}, + "style": {}, + "cta": { + "position": "top_and_bottom", + "clear": {}, + "cancel": {}, + "submit": {} + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/post-table-custom-format.json b/packages/codegen-ui/example-schemas/views/post-table-custom-format.json new file mode 100644 index 000000000..87ee3af03 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/post-table-custom-format.json @@ -0,0 +1,45 @@ +{ + "dataSource": { + "model": "Post", + "predicate": { + "and": [ + { + "field": "username", + "operand": "Guy", + "operator": "notContains" + }, + { + "field": "createdAt", + "operand": "25", + "operator": "contains" + } + ] + }, + "sort": [ + { + "direction": "ASC", + "field": "username" + } + ], + "type": "DataStore" + }, + "id": "v000001", + "name": "MyPostTable", + "schemaVersion": "1.0.0", + "style": {}, + "cta": {}, + "viewConfiguration": { + "type": "Table", + "table": { + "columns": { + "createdAt": { + "valueFormatting": { + "stringFormat": { + "localeDateTimeFormat": "locale" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/post-table-datastore.json b/packages/codegen-ui/example-schemas/views/post-table-datastore.json new file mode 100644 index 000000000..ca97b6cd8 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/post-table-datastore.json @@ -0,0 +1,32 @@ +{ + "dataSource": { + "model": "Post", + "predicate": { + "and": [ + { + "field": "username", + "operand": "username0", + "operator": "notContains" + }, + { + "field": "createdAt", + "operand": "2022", + "operator": "contains" + } + ] + }, + "sort": [ + { + "direction": "ASC", + "field": "username" + } + ], + "type": "DataStore" + }, + "id": "v-0001", + "name": "MyPostTable", + "schemaVersion": "1.0.0", + "style": {}, + "cta": {}, + "viewConfiguration": { "type": "Table", "table": {} } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/table-from-custom-json.json b/packages/codegen-ui/example-schemas/views/table-from-custom-json.json new file mode 100644 index 000000000..34fbae0e9 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/table-from-custom-json.json @@ -0,0 +1,22 @@ +{ + "hasCustomDataSource": true, + "dataSource" : { + "model" : "{\"name\":\"bob\",\"age\":25,\"address\":\"123 street\",\"birthday\":\"5/5/99\"}", + "type" : "Custom" + }, + "id" : "v-3ziKr6s40vGzLDFAJL", + "name" : "CustomTable", + "schemaVersion" : "1.0.0", + "style" : { + "alignment" : { + "value" : "center" + }, + "outerPadding" : { + "value" : "3px" + } + }, + "viewConfiguration" : { + "type" : "Table", + "table": {} + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/table-from-datastore-no-header.json b/packages/codegen-ui/example-schemas/views/table-from-datastore-no-header.json new file mode 100644 index 000000000..7b9b70180 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/table-from-datastore-no-header.json @@ -0,0 +1,38 @@ +{ + "dataSource" : { + "model" : "Person", + "sort" : [ { + "direction" : "ASC", + "field" : "comments" + }, { + "direction" : "ASC", + "field" : "name" + } ], + "type" : "DataStore" + }, + "id" : "v-3ziKr6s40vGzLDFAJL", + "name" : "PersonTable", + "schemaVersion" : "1.0.0", + "style" : { + "alignment" : { + "value" : "center" + }, + "outerPadding" : { + "value" : "3px" + } + }, + "viewConfiguration" : { + "type" : "Table", + "table": { + "disableHeaders": true, + "columns" : { + "name" : { + "excluded" : true + }, + "id" : { + "excluded" : true + } + } + } + } + } \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/table-from-datastore.json b/packages/codegen-ui/example-schemas/views/table-from-datastore.json new file mode 100644 index 000000000..c44944757 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/table-from-datastore.json @@ -0,0 +1,61 @@ +{ + "dataSource" : { + "model" : "Person", + "sort" : [ { + "direction" : "ASC", + "field" : "comments" + }, { + "direction" : "ASC", + "field" : "name" + } ], + "type" : "DataStore" + }, + "id" : "v-3ziKr6s40vGzLDFAJL", + "name" : "PersonTable", + "schemaVersion" : "1.0.0", + "style" : { + "alignment" : { + "value" : "center" + }, + "outerPadding" : { + "value" : "3px" + } + }, + "viewConfiguration" : { + "type" : "Table", + "table": { + "columns" : { + "name" : { + "excluded" : true + }, + "id" : { + "excluded" : true + }, + "hireDate" : { + "valueFormatting": { + "stringFormat": { + "nonLocaleDateTimeFormat": { + "dateFormat": "locale", + "timeFormat": "hours24" + } + } + } + }, + "createdAt" : { + "valueFormatting": { + "stringFormat": { + "timeFormat": "hours24" + } + } + }, + "updatedAt" : { + "valueFormatting": { + "stringFormat": { + "dateFormat": "Mmm, DD YYYY" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/codegen-ui/index.ts b/packages/codegen-ui/index.ts index 0f1f5a350..2be251d92 100644 --- a/packages/codegen-ui/index.ts +++ b/packages/codegen-ui/index.ts @@ -21,6 +21,7 @@ export * from './lib/render-component-response'; export * from './lib/framework-output-manager'; export * from './lib/template-renderer-factory'; export * from './lib/generate-form-definition'; +export * from './lib/generate-view-definition'; export * from './lib/generic-from-datastore'; export * from './lib/renderer-helper'; diff --git a/packages/codegen-ui/lib/__tests__/__utils__/basic-form-definition.ts b/packages/codegen-ui/lib/__tests__/__utils__/basic-form-definition.ts new file mode 100644 index 000000000..a816e1fed --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/__utils__/basic-form-definition.ts @@ -0,0 +1,33 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { FormDefinition } from '../../types'; + +export const getBasicFormDefinition = (): FormDefinition => ({ + form: { + layoutStyle: { + horizontalGap: { value: '15px' }, + verticalGap: { value: '15px' }, + outerPadding: { value: '20px' }, + }, + }, + elements: {}, + elementMatrix: [], + buttons: { + buttonConfigs: {}, + position: 'bottom', + buttonMatrix: [['clear'], ['cancel', 'submit']], + }, +}); diff --git a/packages/codegen-ui/lib/__tests__/__utils__/mock-schemas.ts b/packages/codegen-ui/lib/__tests__/__utils__/mock-schemas.ts index 777d94ff3..f1875e88d 100644 --- a/packages/codegen-ui/lib/__tests__/__utils__/mock-schemas.ts +++ b/packages/codegen-ui/lib/__tests__/__utils__/mock-schemas.ts @@ -783,3 +783,92 @@ export const schemaWithNonModels: Schema = { }, version: '38a1a46479c6cd75d21439d7f3122c1d', }; + +export const schemaWithAssumptions: Schema = { + models: { + User: { + name: 'User', + fields: { + friends: { + name: 'friends', + isArray: true, + type: { + model: 'Friend', + }, + isRequired: false, + attributes: [], + isArrayNullable: true, + association: { + connectionType: 'HAS_MANY', + associatedWith: 'friendId', + }, + }, + posts: { + name: 'posts', + isArray: true, + type: { + model: 'Post', + }, + isRequired: false, + attributes: [], + isArrayNullable: true, + association: { + connectionType: 'HAS_MANY', + associatedWith: 'userPostsId', + }, + }, + badges: { + name: 'badges', + isArray: true, + type: 'String', + isRequired: false, + attributes: [], + isArrayNullable: true, + }, + }, + syncable: true, + pluralName: 'Users', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'key', + properties: { + fields: ['id'], + }, + }, + ], + }, + Event: { + name: 'Post', + fields: { + name: { + name: 'name', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + }, + syncable: true, + pluralName: 'Posts', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'key', + properties: { + fields: ['id'], + }, + }, + ], + }, + }, + enums: {}, + nonModels: {}, + version: 'version', +}; diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts deleted file mode 100644 index c15f04e22..000000000 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/form-to-component.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import { postSchema } from '../__utils__/mock-schemas'; -import { mapFormToComponent } from '../../generate-form-definition/form-to-component'; -import { StudioForm } from '../../types'; - -describe('formToComponent', () => { - it('should map datastore model fields', () => { - const myForm: StudioForm = { - name: 'mySampleForm', - formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Post' }, - fields: {}, - sectionalElements: {}, - style: {}, - }; - - // shallow test of mapper - const component = mapFormToComponent(myForm, postSchema.models.Post); - expect(component).toBeDefined(); - expect(component.children).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: 'mySampleFormGrid', - componentType: 'Grid', - properties: expect.objectContaining({ - columnGap: { value: '1rem' }, - rowGap: { value: '1rem' }, - }), - children: expect.any(Array), - }), - ]), - ); - }); -}); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/generate-form-definition.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/generate-form-definition.test.ts index 7b43e9042..8e95c49ac 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/generate-form-definition.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/generate-form-definition.test.ts @@ -14,44 +14,81 @@ limitations under the License. */ import { generateFormDefinition } from '../../generate-form-definition'; +import { ValidationTypes } from '../../types'; describe('generateFormDefinition', () => { it('should map DataStore model fields', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: {}, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { Dog: { fields: { name: { dataType: 'String', readOnly: false, required: true, isArray: false } } } }, }, - modelInfo: { fields: [{ name: 'name', type: 'String', isReadOnly: false, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements).toStrictEqual({ name: { componentType: 'TextField', - props: { label: 'name', isRequired: true, isReadOnly: false }, + dataType: 'String', + props: { label: 'Name', isRequired: true, isReadOnly: false }, + studioFormComponentType: 'TextField', + validations: [{ type: ValidationTypes.REQUIRED, immutable: true }], }, }); }); + it('should throw if form has source type DataStore, but no schema is available', () => { + expect(() => + generateFormDefinition({ + form: { + id: '123', + name: 'sampleForm', + formActionType: 'create', + dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + fields: {}, + sectionalElements: {}, + style: {}, + cta: {}, + }, + }), + ).toThrow(); + }); + it('should override field configurations from DataStore', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { weight: { inputType: { type: 'SliderField', minValue: 1, maxValue: 100, step: 2 } } }, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { Dog: { fields: { weight: { dataType: 'Float', readOnly: false, required: true, isArray: false } } } }, }, - modelInfo: { fields: [{ name: 'weight', type: 'Float', isReadOnly: false, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements).toStrictEqual({ weight: { componentType: 'SliderField', - props: { label: 'weight', min: 1, max: 100, step: 2, isDisabled: false, isRequired: true }, + dataType: 'Float', + props: { label: 'Weight', min: 1, max: 100, step: 2, isDisabled: false, isRequired: true }, + validations: [{ type: ValidationTypes.REQUIRED, immutable: true }], }, }); }); @@ -59,14 +96,21 @@ describe('generateFormDefinition', () => { it('should not add overrides to the matrix', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { weight: { inputType: { type: 'SliderField', minValue: 1, maxValue: 100, step: 2 } } }, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { Dog: { fields: { weight: { dataType: 'Float', readOnly: false, required: true, isArray: false } } } }, }, - modelInfo: { fields: [{ name: 'weight', type: 'Float', isReadOnly: false, isRequired: true, isArray: false }] }, }); expect(formDefinition.elementMatrix).toStrictEqual([['weight']]); @@ -75,14 +119,16 @@ describe('generateFormDefinition', () => { it('should add fields that do not exist in DataStore', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { weight: { inputType: { type: 'SliderField', minValue: 1, maxValue: 100, step: 2 } } }, sectionalElements: {}, style: {}, + cta: {}, }, - modelInfo: { fields: [] }, + dataSchema: { dataSourceType: 'DataStore', enums: {}, nonModels: {}, models: { Dog: { fields: {} } } }, }); expect(formDefinition.elements).toStrictEqual({ weight: { componentType: 'SliderField', props: { min: 1, max: 100, step: 2, label: 'Label' } }, @@ -92,14 +138,16 @@ describe('generateFormDefinition', () => { it('should add fields that do not exist in DataStore to the matrix', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { weight: { inputType: { type: 'SliderField', minValue: 1, maxValue: 100, step: 2 } } }, sectionalElements: {}, style: {}, + cta: {}, }, - modelInfo: { fields: [] }, + dataSchema: { dataSourceType: 'DataStore', enums: {}, nonModels: {}, models: { Dog: { fields: {} } } }, }); expect(formDefinition.elementMatrix).toStrictEqual([['weight']]); }); @@ -107,16 +155,17 @@ describe('generateFormDefinition', () => { it('should add sectional elements', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + dataType: { dataSourceType: 'Custom', dataTypeName: 'dfjkajfl' }, fields: {}, sectionalElements: { Heading123: { type: 'Heading', position: { fixed: 'first' }, level: 1, text: 'Create Dog' }, }, style: {}, + cta: {}, }, - modelInfo: { fields: [{ name: 'weight', type: 'Float', isReadOnly: false, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements.Heading123).toStrictEqual({ componentType: 'Heading', @@ -132,14 +181,15 @@ describe('generateFormDefinition', () => { }; const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + dataType: { dataSourceType: 'Custom', dataTypeName: 'dfsdjflk' }, fields: {}, sectionalElements: {}, style, + cta: {}, }, - modelInfo: { fields: [] }, }); expect(formDefinition.form.layoutStyle).toStrictEqual(style); }); @@ -147,6 +197,7 @@ describe('generateFormDefinition', () => { it('should not leave empty rows in the matrix', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, @@ -160,13 +211,21 @@ describe('generateFormDefinition', () => { }, style: {}, + cta: {}, }, - modelInfo: { - fields: [ - { name: 'name', type: 'String', isReadOnly: false, isRequired: true, isArray: false }, - { name: 'weight', type: 'Float', isReadOnly: false, isRequired: true, isArray: false }, - { name: 'age', type: 'Int', isReadOnly: false, isRequired: true, isArray: false }, - ], + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: false, required: true, isArray: false }, + weight: { dataType: 'Float', readOnly: false, required: true, isArray: false }, + age: { dataType: 'Int', readOnly: false, required: true, isArray: false }, + }, + }, + }, }, }); expect(formDefinition.elementMatrix).toStrictEqual([['Heading123']]); @@ -175,6 +234,7 @@ describe('generateFormDefinition', () => { it('should correctly map positions', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, @@ -188,13 +248,21 @@ describe('generateFormDefinition', () => { }, style: {}, + cta: {}, }, - modelInfo: { - fields: [ - { name: 'name', type: 'String', isReadOnly: false, isRequired: true, isArray: false }, - { name: 'weight', type: 'Float', isReadOnly: false, isRequired: true, isArray: false }, - { name: 'age', type: 'Int', isReadOnly: false, isRequired: true, isArray: false }, - ], + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: false, required: true, isArray: false }, + weight: { dataType: 'Float', readOnly: false, required: true, isArray: false }, + age: { dataType: 'Int', readOnly: false, required: true, isArray: false }, + }, + }, + }, }, }); expect(formDefinition.elementMatrix).toStrictEqual([['Heading123'], ['name', 'age', 'weight']]); @@ -204,9 +272,10 @@ describe('generateFormDefinition', () => { it('should requeue if related element is not yet found', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + dataType: { dataSourceType: 'Custom', dataTypeName: 'dfjslkfj' }, fields: { color: { position: { below: 'name' }, inputType: { type: 'TextField' } }, weight: { position: { rightOf: 'age' }, inputType: { type: 'TextField' } }, @@ -218,9 +287,7 @@ it('should requeue if related element is not yet found', () => { }, style: {}, - }, - modelInfo: { - fields: [], + cta: {}, }, }); expect(formDefinition.elementMatrix).toStrictEqual([['Heading123'], ['name', 'age', 'weight'], ['color']]); @@ -229,9 +296,10 @@ it('should requeue if related element is not yet found', () => { it('should handle fields without position', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + dataType: { dataSourceType: 'Custom', dataTypeName: 'fjsldkfj' }, fields: { color: { position: { below: 'name' }, inputType: { type: 'TextField' } }, weight: { position: { rightOf: 'age' }, inputType: { type: 'TextField' } }, @@ -244,9 +312,7 @@ it('should handle fields without position', () => { }, style: {}, - }, - modelInfo: { - fields: [], + cta: {}, }, }); expect(formDefinition.elementMatrix).toStrictEqual([['Heading123'], ['name', 'age', 'weight'], ['color'], ['bark']]); @@ -255,16 +321,15 @@ it('should handle fields without position', () => { it('should fill out styles using defaults', () => { const definitionForFormWithoutStyle = generateFormDefinition({ form: { + id: '123', name: 'mySampleForm', formActionType: 'create', - dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, + dataType: { dataSourceType: 'Custom', dataTypeName: 'dfkjad' }, fields: {}, sectionalElements: {}, style: {}, - }, - modelInfo: { - fields: [], + cta: {}, }, }); @@ -278,14 +343,27 @@ it('should fill out styles using defaults', () => { it('should skip read-only fields without overrides', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: {}, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: true, required: true, isArray: false }, + }, + }, + }, }, - modelInfo: { fields: [{ name: 'name', type: 'String', isReadOnly: true, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements).toStrictEqual({}); expect(formDefinition.elementMatrix).toStrictEqual([]); @@ -294,19 +372,35 @@ it('should skip read-only fields without overrides', () => { it('should add read-only fields if it has overrides', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { name: { inputType: { type: 'TextField' } } }, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: true, required: true, isArray: false }, + }, + }, + }, }, - modelInfo: { fields: [{ name: 'name', type: 'String', isReadOnly: true, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements).toStrictEqual({ name: { componentType: 'TextField', - props: { label: 'name', isRequired: true, isReadOnly: true }, + dataType: 'String', + props: { label: 'Name', isRequired: true, isReadOnly: true }, + studioFormComponentType: 'TextField', + validations: [{ type: ValidationTypes.REQUIRED, immutable: true }], }, }); expect(formDefinition.elementMatrix).toStrictEqual([['name']]); @@ -315,14 +409,27 @@ it('should add read-only fields if it has overrides', () => { it('should skip adding id field if it has no overrides', () => { const formDefinition = generateFormDefinition({ form: { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: {}, sectionalElements: {}, style: {}, + cta: {}, + }, + dataSchema: { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + id: { dataType: 'ID', readOnly: false, required: true, isArray: false }, + }, + }, + }, }, - modelInfo: { fields: [{ name: 'id', type: 'ID', isReadOnly: false, isRequired: true, isArray: false }] }, }); expect(formDefinition.elements).toStrictEqual({}); expect(formDefinition.elementMatrix).toStrictEqual([]); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts deleted file mode 100644 index 2e2a38789..000000000 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/datastore-model.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { addDataStoreModelField } from '../../../generate-form-definition/helpers'; -import { FormDefinition, ModelFieldsConfigs } from '../../../types'; - -describe('addDataStoreModelField', () => { - it('should map to elementMatrix and add to modelFieldsConfigs', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; - - const dataStoreModelField = { name: 'name', type: 'String', isReadOnly: false, isRequired: false, isArray: false }; - - const modelFieldsConfigs: ModelFieldsConfigs = {}; - - addDataStoreModelField(formDefinition, modelFieldsConfigs, dataStoreModelField); - - expect(formDefinition.elementMatrix).toStrictEqual([['name']]); - expect(modelFieldsConfigs.name).toStrictEqual({ - label: 'name', - inputType: { type: 'TextField', required: false, readOnly: false, name: 'name', value: 'true' }, - }); - }); - - it('should throw if field is an array', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; - - const dataStoreModelField = { name: 'name', type: 'String', isReadOnly: false, isRequired: false, isArray: true }; - - expect(() => addDataStoreModelField(formDefinition, {}, dataStoreModelField)).toThrow(); - }); - - it('should throw if there is no default component', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; - - const dataStoreModelField = { - name: 'name', - type: 'ErrantType', - isReadOnly: false, - isRequired: false, - isArray: false, - }; - - expect(() => addDataStoreModelField(formDefinition, {}, dataStoreModelField)).toThrow(); - }); - - it('should skip generation of id field from data store model', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; - - const dataStoreModelField = { name: 'id', type: 'ID', isReadOnly: true, isRequired: true, isArray: false }; - - const modelFieldsConfigs: ModelFieldsConfigs = {}; - - addDataStoreModelField(formDefinition, modelFieldsConfigs, dataStoreModelField); - - expect(formDefinition.elementMatrix).toStrictEqual([]); - expect(modelFieldsConfigs).toStrictEqual({ - id: { - inputType: { - name: 'id', - readOnly: true, - required: true, - type: 'TextField', - value: 'true', - }, - label: 'id', - }, - }); - }); - - it('should skip generation of read only fields from data store model', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; - - const dataStoreModelField = { name: 'name', type: 'Boolean', isReadOnly: true, isRequired: false, isArray: false }; - - const modelFieldsConfigs: ModelFieldsConfigs = {}; - - addDataStoreModelField(formDefinition, modelFieldsConfigs, dataStoreModelField); - - expect(formDefinition.elementMatrix).toStrictEqual([]); - expect(modelFieldsConfigs).toStrictEqual({ - name: { - inputType: { - name: 'name', - readOnly: true, - required: false, - type: 'SwitchField', - value: 'true', - }, - label: 'name', - }, - }); - }); -}); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts index 6f213b8a6..53be51fb9 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts @@ -15,7 +15,17 @@ */ import { mapFormFieldConfig, getFormDefinitionInputElement } from '../../../generate-form-definition/helpers'; -import { FormDefinition, ModelFieldsConfigs, StudioFormFieldConfig, StudioGenericFieldConfig } from '../../../types'; +import { mergeValueMappings } from '../../../generate-form-definition/helpers/form-field'; +import { + FormDefinition, + GenericValidationType, + ModelFieldsConfigs, + StringLengthValidationType, + StudioFormFieldConfig, + StudioGenericFieldConfig, + ValidationTypes, +} from '../../../types'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; describe('mapFormFieldConfig', () => { it('should map fields', () => { @@ -30,15 +40,12 @@ describe('mapFormFieldConfig', () => { maxValue: 100, step: 1, readOnly: true, - required: true, }, }, }; const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: [['price']], }; @@ -57,7 +64,7 @@ describe('mapFormFieldConfig', () => { expect(formDefinition.elements.price).toStrictEqual({ componentType: 'SliderField', - props: { label: 'Price', isDisabled: true, min: 0, max: 100, step: 1, isRequired: true }, + props: { label: 'Price', isDisabled: true, min: 0, max: 100, step: 1, isRequired: false }, }); }); @@ -68,9 +75,7 @@ describe('mapFormFieldConfig', () => { }; const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: [['price']], }; @@ -111,6 +116,148 @@ describe('getFormDefinitionInputElement', () => { placeholder: 'MyPlaceholder', defaultValue: 'MyDefaultValue', }, + studioFormComponentType: 'TextField', + }); + }); + + it('should get NumberField', () => { + const config = { + inputType: { + type: 'NumberField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + type: 'number', + }, + studioFormComponentType: 'NumberField', + }); + }); + + it('should get DateField', () => { + const config = { + inputType: { + type: 'DateField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + type: 'date', + }, + studioFormComponentType: 'DateField', + }); + }); + + it('should get TimeField', () => { + const config = { + inputType: { + type: 'TimeField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + type: 'time', + }, + studioFormComponentType: 'TimeField', + }); + }); + + it('should get DateTimeField', () => { + const config = { + inputType: { + type: 'DateTimeField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + type: 'datetime-local', + }, + studioFormComponentType: 'DateTimeField', + }); + }); + + it('should get IPAddressField', () => { + const config = { + inputType: { + type: 'IPAddressField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + }, + studioFormComponentType: 'IPAddressField', + validations: [{ type: ValidationTypes.IP_ADDRESS, immutable: true }], + }); + }); + + it('should get URLField', () => { + const config = { + inputType: { + type: 'URLField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + }, + studioFormComponentType: 'URLField', + validations: [{ type: ValidationTypes.URL, immutable: true }], + }); + }); + + it('should get EmailField', () => { + const config = { + inputType: { + type: 'EmailField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + }, + studioFormComponentType: 'EmailField', + validations: [{ type: ValidationTypes.EMAIL, immutable: true }], + }); + }); + + it('should add validation if field is required', () => { + const config = { + inputType: { + type: 'EmailField', + required: true, + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextField', + props: { + label: 'Label', + isRequired: true, + }, + studioFormComponentType: 'EmailField', + validations: [ + { type: ValidationTypes.REQUIRED, immutable: true }, + { type: ValidationTypes.EMAIL, immutable: true }, + ], }); }); @@ -139,6 +286,7 @@ describe('getFormDefinitionInputElement', () => { expect(getFormDefinitionInputElement(config)).toStrictEqual({ componentType: 'PhoneNumberField', props: { label: 'Label', defaultCountryCode: '+11' }, + validations: [{ type: ValidationTypes.PHONE, immutable: true }], }); }); @@ -147,21 +295,18 @@ describe('getFormDefinitionInputElement', () => { inputType: { type: 'SelectField', readOnly: true, - valueMappings: [ - { displayValue: 'value1Display', value: 'value1' }, - { displayValue: 'value2Display', value: 'value2' }, - ], + valueMappings: { values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }] }, defaultValue: 'value1', }, }; expect(getFormDefinitionInputElement(config)).toStrictEqual({ componentType: 'SelectField', - props: { label: 'Label', isDisabled: true }, - options: [ - { value: 'value1', children: 'value1Display' }, - { value: 'value2', children: 'value2Display' }, - ], + props: { label: 'Label', isDisabled: true, placeholder: 'Please select an option' }, + valueMappings: { + values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }], + bindingProperties: {}, + }, defaultValue: 'value1', }); }); @@ -193,6 +338,55 @@ describe('getFormDefinitionInputElement', () => { placeholder: 'MyPlaceholder', defaultValue: 'MyDefaultValue', }, + studioFormComponentType: 'TextAreaField', + }); + }); + + it('should get JSONField', () => { + const config = { + inputType: { + type: 'JSONField', + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'TextAreaField', + props: { + label: 'Label', + }, + studioFormComponentType: 'JSONField', + validations: [{ type: ValidationTypes.JSON, immutable: true }], + }); + }); + + it('should merge validations', () => { + const configValidation: StringLengthValidationType = { + type: ValidationTypes.LESS_THAN_CHAR_LENGTH, + numValues: [4], + }; + const config = { + inputType: { + type: 'JSONField', + }, + validations: [configValidation], + }; + + const baseConfigValidation: GenericValidationType = { type: ValidationTypes.REQUIRED }; + + const baseConfig = { + inputType: { + type: 'JSONField', + }, + validations: [baseConfigValidation], + }; + + expect(getFormDefinitionInputElement(config, baseConfig)).toStrictEqual({ + componentType: 'TextAreaField', + props: { + label: 'Label', + }, + studioFormComponentType: 'JSONField', + validations: [{ type: ValidationTypes.JSON, immutable: true }, baseConfigValidation, configValidation], }); }); @@ -280,7 +474,7 @@ describe('getFormDefinitionInputElement', () => { expect(getFormDefinitionInputElement(config)).toStrictEqual({ componentType: 'CheckboxField', - props: { label: 'Label', name: 'fieldName', value: 'true', defaultChecked: true }, + props: { label: 'Label', name: 'fieldName', value: 'fieldName', defaultChecked: true }, }); }); @@ -289,20 +483,63 @@ describe('getFormDefinitionInputElement', () => { inputType: { type: 'RadioGroupField', name: 'MyFieldName', - valueMappings: [ - { displayValue: 'value1Display', value: 'value1' }, - { displayValue: 'value2Display', value: 'value2' }, - ], + valueMappings: { values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }] }, }, }; expect(getFormDefinitionInputElement(config)).toStrictEqual({ componentType: 'RadioGroupField', props: { label: 'Label', name: 'MyFieldName' }, - radios: [ - { value: 'value1', children: 'value1Display' }, - { value: 'value2', children: 'value2Display' }, - ], + valueMappings: { + values: [{ value: { value: 'value1' }, displayvalue: { value: 'displayValue1' } }], + bindingProperties: {}, + }, + }); + }); + + it('should return default valueMappings for RadioGroupField if no values available', () => { + const config = { + inputType: { + type: 'RadioGroupField', + name: 'MyFieldName', + valueMappings: { values: [] }, + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + componentType: 'RadioGroupField', + props: { label: 'Label', name: 'MyFieldName' }, + valueMappings: { + values: [{ value: { value: 'Option' } }], + }, + }); + }); + + it('should handle valueMappings for RadioGroupField of Boolean type', () => { + const config: StudioFormFieldConfig = { + dataType: 'Boolean', + inputType: { + type: 'RadioGroupField', + name: 'MyFieldName', + valueMappings: { + values: [ + { displayValue: { value: 'Yup' }, value: { value: 'true' } }, + { displayValue: { value: 'Should not be mapped' }, value: { value: 'should not be mapped' } }, + ], + }, + }, + }; + + expect(getFormDefinitionInputElement(config)).toStrictEqual({ + dataType: 'Boolean', + componentType: 'RadioGroupField', + props: { label: 'Label', name: 'MyFieldName' }, + valueMappings: { + values: [ + { value: { value: 'true' }, displayValue: { value: 'Yup' } }, + { value: { value: 'false' }, displayValue: { value: 'No' } }, + ], + }, }); }); @@ -322,3 +559,61 @@ describe('getFormDefinitionInputElement', () => { expect(() => getFormDefinitionInputElement(config)).toThrow(); }); }); + +describe('mergeValueMappings', () => { + it('should return override values if no base values', () => { + expect(mergeValueMappings(undefined, { values: [{ value: { value: 'value1' } }] }).values).toStrictEqual([ + { value: { value: 'value1' } }, + ]); + }); + + it('should return base values if no override values', () => { + expect(mergeValueMappings({ values: [{ value: { value: 'value1' } }] }, undefined).values).toStrictEqual([ + { value: { value: 'value1' } }, + ]); + }); + + it('should only return base values with overrides applied if both base and overrides present', () => { + const mergedMappings = mergeValueMappings( + { + values: [ + { value: { value: 'NEW_YORK' }, displayValue: { value: 'New york' } }, + { value: { value: 'HOUSTON' }, displayValue: { value: 'Houston' } }, + { value: { value: 'LOS_ANGELES' }, displayValue: { value: 'Los angeles' } }, + ], + }, + { + values: [ + { value: { value: 'LOS_ANGELES' }, displayValue: { value: 'LA' } }, + { value: { value: 'AUSTIN' }, displayValue: { value: 'Austin' } }, + ], + }, + ); + + expect(mergedMappings.values).toStrictEqual([ + { value: { value: 'NEW_YORK' }, displayValue: { value: 'New york' } }, + { value: { value: 'HOUSTON' }, displayValue: { value: 'Houston' } }, + { value: { value: 'LOS_ANGELES' }, displayValue: { value: 'LA' } }, + ]); + + expect(mergedMappings.values.find((v) => 'value' in v.value && v.value.value === 'AUSTIN')).toBeUndefined(); + }); + + it('should merge base and override bindingProperties', () => { + expect( + mergeValueMappings( + { + values: [{ value: { value: 'sdjoiflj' }, displayValue: { bindingProperties: { property: 'Dog' } } }], + bindingProperties: { + Dog: { type: 'Data', bindingProperties: { model: 'Dog' } }, + Person: { type: 'Data', bindingProperties: { model: 'Person' } }, + }, + }, + { values: [], bindingProperties: { Dog: { type: 'Data', bindingProperties: { model: 'MyDog' } } } }, + ).bindingProperties, + ).toStrictEqual({ + Person: { type: 'Data', bindingProperties: { model: 'Person' } }, + Dog: { type: 'Data', bindingProperties: { model: 'MyDog' } }, + }); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/map-elements.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/map-elements.test.ts index e3d60ceac..ec110144d 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/map-elements.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/map-elements.test.ts @@ -15,6 +15,7 @@ */ import { mapElements } from '../../../generate-form-definition/helpers'; import { FormDefinition, SectionalElement, ModelFieldsConfigs, StudioForm } from '../../../types'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; describe('mapElements', () => { it('should map sectional elements & input elements with and without overrides', () => { @@ -25,9 +26,7 @@ describe('mapElements', () => { }; const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: [['myText', 'name'], ['price']], }; @@ -47,21 +46,24 @@ describe('mapElements', () => { }; const form: StudioForm = { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: { name: { inputType: { type: 'TextField' } } }, sectionalElements: { myText: sectionalConfig }, style: {}, + cta: {}, }; mapElements({ formDefinition, modelFieldsConfigs, form }); expect(formDefinition.elements).toStrictEqual({ myText: { componentType: 'Text', props: { children: 'MyText' } }, - name: { componentType: 'TextField', props: { label: 'Label' } }, + name: { componentType: 'TextField', props: { label: 'Label' }, studioFormComponentType: 'TextField' }, price: { componentType: 'TextField', props: { label: 'price', isRequired: false, isReadOnly: false }, + studioFormComponentType: 'TextField', }, }); @@ -70,21 +72,21 @@ describe('mapElements', () => { it('should throw if config for element not found', () => { const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: [['myText']], }; const modelFieldsConfigs: ModelFieldsConfigs = {}; const form: StudioForm = { + id: '123', name: 'sampleForm', formActionType: 'create', dataType: { dataSourceType: 'DataStore', dataTypeName: 'Dog' }, fields: {}, sectionalElements: {}, style: {}, + cta: {}, }; expect(() => mapElements({ formDefinition, modelFieldsConfigs, form })).toThrow(); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts new file mode 100644 index 000000000..ea8f77bba --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/model-fields-configs.test.ts @@ -0,0 +1,305 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { mapModelFieldsConfigs, getFieldTypeMapKey } from '../../../generate-form-definition/helpers'; +import { FormDefinition, GenericDataSchema } from '../../../types'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; + +describe('mapModelFieldsConfigs', () => { + it('should map to elementMatrix and add to modelFieldsConfigs', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: false, required: false, isArray: true }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(formDefinition.elementMatrix).toStrictEqual([['name']]); + expect(modelFieldsConfigs.name).toStrictEqual({ + label: 'Name', + dataType: 'String', + inputType: { type: 'TextField', isArray: true, required: false, readOnly: false, name: 'name', value: 'name' }, + }); + }); + + it('should properly map different field names casings to sentence case', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: false, required: false, isArray: false }, + camelCaseField: { dataType: 'String', readOnly: false, required: false, isArray: false }, + 'param-case-field': { dataType: 'String', readOnly: false, required: false, isArray: false }, + snake_case_field: { dataType: 'String', readOnly: false, required: false, isArray: false }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(Object.values(modelFieldsConfigs).map((m) => m.label)).toStrictEqual([ + 'Name', + 'Camel case field', + 'Param case field', + 'Snake case field', + ]); + }); + + it('should throw if specified model is not found', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: false, required: false, isArray: false }, + }, + }, + }, + }; + + expect(() => mapModelFieldsConfigs({ dataTypeName: 'Cat', formDefinition, dataSchema })).toThrow(); + }); + + it('should generate config from id field but not add it to matrix', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + id: { dataType: 'ID', readOnly: false, required: true, isArray: false }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(formDefinition.elementMatrix).toStrictEqual([]); + expect(modelFieldsConfigs).toStrictEqual({ + id: { + dataType: 'ID', + inputType: { + name: 'id', + readOnly: false, + required: true, + type: 'TextField', + value: 'id', + isArray: false, + }, + label: 'Id', + }, + }); + }); + + it('should add read-only fields to configs but not to matrix', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + name: { dataType: 'String', readOnly: true, required: false, isArray: false }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(formDefinition.elementMatrix).toStrictEqual([]); + expect(modelFieldsConfigs).toStrictEqual({ + name: { + dataType: 'String', + inputType: { + name: 'name', + readOnly: true, + required: false, + type: 'TextField', + value: 'name', + isArray: false, + }, + label: 'Name', + }, + }); + }); + + it('should add relationship fields to configs but not to matrix', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + ownerId: { + dataType: 'ID', + readOnly: true, + required: false, + isArray: false, + relationship: { type: 'HAS_ONE', relatedModelName: 'Owner' }, + }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(formDefinition.elementMatrix).toStrictEqual([]); + expect(modelFieldsConfigs).toStrictEqual({ + ownerId: { + dataType: 'ID', + inputType: { + name: 'ownerId', + readOnly: true, + required: false, + type: 'SelectField', + value: 'ownerId', + isArray: false, + }, + label: 'Owner id', + }, + }); + }); + + it('should add value mappings from enums', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const nonEnglishAlphabetTest = 'ㅎ🌱يَّة'; + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: { City: { values: ['NEW_YORK', 'HOUSTON', 'LOS_ANGELES', nonEnglishAlphabetTest] } }, + nonModels: {}, + models: { + Dog: { + fields: { + city: { dataType: { enum: 'City' }, readOnly: false, required: false, isArray: false }, + }, + }, + }, + }; + + const modelFieldsConfigs = mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema }); + + expect(modelFieldsConfigs).toStrictEqual({ + city: { + dataType: { + enum: 'City', + }, + inputType: { + name: 'city', + readOnly: false, + required: false, + type: 'SelectField', + value: 'city', + valueMappings: { + values: [ + { value: { value: 'NEW_YORK' }, displayValue: { value: 'New york' } }, + { value: { value: 'HOUSTON' }, displayValue: { value: 'Houston' } }, + { value: { value: 'LOS_ANGELES' }, displayValue: { value: 'Los angeles' } }, + { value: { value: nonEnglishAlphabetTest }, displayValue: { value: nonEnglishAlphabetTest } }, + ], + }, + isArray: false, + }, + label: 'City', + }, + }); + }); + + it('should throw if type is enum but no matching enum provided', () => { + const formDefinition: FormDefinition = getBasicFormDefinition(); + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + Dog: { + fields: { + city: { dataType: { enum: 'City' }, readOnly: false, required: false, isArray: false }, + }, + }, + }, + }; + + expect(() => mapModelFieldsConfigs({ dataTypeName: 'Dog', formDefinition, dataSchema })).toThrow(); + }); +}); + +describe('getFieldTypeMapKey', () => { + it('should return `Relationship` if field is of type model or has a related model', () => { + expect( + getFieldTypeMapKey({ + dataType: { model: 'Dog' }, + readOnly: false, + required: false, + isArray: false, + }), + ).toBe('Relationship'); + + expect( + getFieldTypeMapKey({ + dataType: 'ID', + readOnly: false, + required: false, + isArray: false, + relationship: { relatedModelName: 'Dog', type: 'HAS_ONE' }, + }), + ).toBe('Relationship'); + }); + + it('should return `NonModel` if dataType is nonModel', () => { + expect( + getFieldTypeMapKey({ + dataType: { nonModel: 'Misc' }, + readOnly: false, + required: false, + isArray: false, + }), + ).toBe('NonModel'); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/position.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/position.test.ts index 7cd45905b..923411ed5 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/position.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/position.test.ts @@ -14,7 +14,13 @@ limitations under the License. */ -import { findIndices, removeAndReturnItemOnward, removeFromMatrix } from '../../../generate-form-definition/helpers'; +import { + findIndices, + mapElementMatrix, + removeAndReturnItemOnward, + removeFromMatrix, +} from '../../../generate-form-definition/helpers'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; describe('findIndices', () => { it('should find the indices of a string in a two-dimensional array', () => { @@ -34,9 +40,7 @@ describe('removeFromMatrix', () => { const matrix = [['one', 'two', 'three', 'four'], ['five'], ['six', 'seven', 'eight']]; const formDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: matrix, }; @@ -51,9 +55,7 @@ describe('removeAndReturnItemOnward', () => { const matrix = [['one', 'two', 'three', 'four'], ['five'], ['six', 'seven', 'eight']]; const formDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, + ...getBasicFormDefinition(), elementMatrix: matrix, }; @@ -62,3 +64,68 @@ describe('removeAndReturnItemOnward', () => { expect(formDefinition.elementMatrix).toStrictEqual([['one'], ['five'], ['six', 'seven', 'eight']]); }); }); + +describe('mapElementMatrix', () => { + describe('vertical positioning', () => { + it('should throw if element is positioned relative to non-existing name', () => { + const elementQueue = [ + { name: 'a', position: { below: 'b' } }, + { name: 'b', position: { below: 'c' } }, + { name: 'c', position: { below: 'd' } }, + ]; + expect(() => mapElementMatrix({ elementQueue, formDefinition: getBasicFormDefinition() })).toThrow(); + }); + + it('should throw if there is a circular dependency', () => { + const elementQueue = [ + { name: 'a', position: { below: 'b' } }, + { name: 'b', position: { below: 'c' } }, + { name: 'c', position: { below: 'a' } }, + ]; + expect(() => mapElementMatrix({ elementQueue, formDefinition: getBasicFormDefinition() })).toThrow(); + }); + }); + + describe('horizontal positioning', () => { + it('should throw if element is positioned relative to non-existing name', () => { + const elementQueue = [ + { name: 'a', position: { rightOf: 'b' } }, + { name: 'b', position: { rightOf: 'c' } }, + { name: 'c', position: { rightOf: 'd' } }, + ]; + expect(() => mapElementMatrix({ elementQueue, formDefinition: getBasicFormDefinition() })).toThrow(); + }); + + it('should throw if there is a circular dependency', () => { + const elementQueue = [ + { name: 'a', position: { rightOf: 'b' } }, + { name: 'b', position: { rightOf: 'c' } }, + { name: 'c', position: { rightOf: 'a' } }, + ]; + expect(() => mapElementMatrix({ elementQueue, formDefinition: getBasicFormDefinition() })).toThrow(); + }); + }); + + describe('two-dimensional layout', () => { + it('should map positions', () => { + const elementQueue = [ + { name: 'g', position: { rightOf: 'f' } }, + { name: 'f', position: { below: 'd' } }, + { name: 'e', position: { rightOf: 'd' } }, + { name: 'd', position: { below: 'a' } }, + { name: 'c', position: { rightOf: 'b' } }, + { name: 'b', position: { rightOf: 'a' } }, + { name: 'a' }, + ]; + const formDefinition = getBasicFormDefinition(); + + mapElementMatrix({ elementQueue, formDefinition }); + + expect(formDefinition.elementMatrix).toStrictEqual([ + ['a', 'b', 'c'], + ['d', 'e'], + ['f', 'g'], + ]); + }); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/sectional-element.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/sectional-element.test.ts index d9bb88308..bf4b773f1 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/sectional-element.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/sectional-element.test.ts @@ -16,14 +16,13 @@ import { mapSectionalElement, getFormDefinitionSectionalElement } from '../../../generate-form-definition/helpers'; import { FormDefinition, SectionalElement } from '../../../types'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; describe('mapSectionalElement', () => { it('should throw if there is already an element with the same name', () => { const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, + ...getBasicFormDefinition(), elements: { Heading123: { componentType: 'Heading', props: {} } }, - buttons: {}, - elementMatrix: [], }; const element: { name: string; config: SectionalElement } = { @@ -35,12 +34,7 @@ describe('mapSectionalElement', () => { }); it('should map configurations', () => { - const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, - elements: {}, - buttons: {}, - elementMatrix: [], - }; + const formDefinition: FormDefinition = getBasicFormDefinition(); const element: { name: string; config: SectionalElement } = { name: 'Heading123', diff --git a/packages/codegen-ui/lib/__tests__/generate-view-definition/generate-table-definition.test.ts b/packages/codegen-ui/lib/__tests__/generate-view-definition/generate-table-definition.test.ts new file mode 100644 index 000000000..159cf87f4 --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/generate-view-definition/generate-table-definition.test.ts @@ -0,0 +1,177 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { + ColumnInfo, + GenericDataSchema, + StudioView, + ViewConfiguration, + ViewDataTypeConfig, + ViewStyle, +} from '../../types'; +import { generateTableDefinition } from '../../generate-view-definition/generate-table-definition'; + +describe('generateTableDefinition', () => { + test('can generate table definition', () => { + const view: StudioView = { + appId: 'appId', + environmentName: 'staging', + id: 'viewId', + name: 'TableOne', + schemaVersion: '1.0', + sourceId: 'source', + dataSource: { + type: 'DataStore', + model: 'TestModel', + }, + style: { + alignment: { + value: 'left', + }, + outerPadding: { + value: '10px', + }, + }, + viewConfiguration: { + type: 'Table', + table: { + columns: { + header1: { + position: { + rightOf: 'header4', + }, + }, + header4: { + excluded: true, + }, + }, + }, + }, + }; + + const dataSchema: GenericDataSchema = { + dataSourceType: 'DataStore', + enums: {}, + nonModels: {}, + models: { + TestModel: { + fields: {}, + }, + }, + }; + + for (let i = 1; i <= 5; i += 1) { + dataSchema.models.TestModel.fields[`header${i}`] = { + dataType: 'String', + isArray: false, + readOnly: false, + required: false, + }; + } + + const definition = generateTableDefinition(view, dataSchema); + + const expectedStyle: ViewStyle = { + alignment: { + value: 'left', + }, + outerPadding: { + value: '10px', + }, + horizontalGap: { + value: '0', + }, + verticalGap: { + value: '0', + }, + }; + + const expectedConfig: ViewConfiguration = { + type: 'Table', + table: { + disableHeaders: false, + highlightOnHover: false, + enableOnRowClick: false, + }, + }; + + const expectedSource: ViewDataTypeConfig = { + type: 'DataStore', + model: 'TestModel', + }; + + const expectedColumns: ColumnInfo[] = [ + { + header: 'header2', + }, + { + header: 'header3', + }, + { + header: 'header1', + position: { + rightOf: 'header4', + }, + }, + { + header: 'header5', + }, + ]; + + expect(definition.tableStyle).toStrictEqual(expectedStyle); + expect(definition.tableConfig).toStrictEqual(expectedConfig); + expect(definition.tableDataSource).toStrictEqual(expectedSource); + expect(definition.columns).toStrictEqual(expectedColumns); + }); + + test('can generate table definition with custom data model', () => { + const view: StudioView = { + appId: 'appId', + environmentName: 'staging', + id: 'viewId', + name: 'CusomTable', + schemaVersion: '1.0', + sourceId: 'source', + dataSource: { + type: 'Custom', + model: '{"name":"bob","age":25,"address":"123 street","birthday":"5/5/99"}', + }, + style: {}, + viewConfiguration: { + type: 'Table', + table: {}, + }, + }; + + const definition = generateTableDefinition(view); + + const expectedColumns: ColumnInfo[] = [ + { + header: 'name', + }, + { + header: 'age', + }, + { + header: 'address', + }, + { + header: 'birthday', + }, + ]; + + expect(definition.columns).toStrictEqual(expectedColumns); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/generate-view-definition/order-filter-columns.test.ts b/packages/codegen-ui/lib/__tests__/generate-view-definition/order-filter-columns.test.ts new file mode 100644 index 000000000..636c63162 --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/generate-view-definition/order-filter-columns.test.ts @@ -0,0 +1,120 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { ColumnsMap, DataStoreModelField } from '../../types'; +import { orderAndFilterVisibleColumns } from '../../generate-view-definition/helpers'; + +describe('orderAndFilterVisibleColumns', () => { + const generateHeaders = (num: number) => { + const fields: DataStoreModelField[] = []; + for (let i = 1; i <= num; i += 1) { + fields.push({ + name: `header${i}`, + type: 'String', + isReadOnly: false, + isArray: false, + isRequired: false, + }); + } + return fields; + }; + + test('default order applied', () => { + const fields = generateHeaders(10); + + const columns = orderAndFilterVisibleColumns({}, fields); + + const headers = columns.map((col) => col.header); + + expect(headers).toStrictEqual([ + 'header1', + 'header2', + 'header3', + 'header4', + 'header5', + 'header6', + 'header7', + 'header8', + 'header9', + 'header10', + ]); + }); + + test('empty data store fields should not throw error', () => { + const fields: DataStoreModelField[] = []; + + expect(() => orderAndFilterVisibleColumns({}, fields)).not.toThrowError(); + }); + + test('should order and filter', () => { + const map: ColumnsMap = { + header1: { + position: { + rightOf: 'header3', + }, + }, + header2: { + position: { + rightOf: 'header4', + }, + }, + header6: { + position: { + fixed: 'first', + }, + }, + header3: { + position: { + rightOf: 'header6', + }, + }, + }; + + const fields = generateHeaders(10); + + const columns = orderAndFilterVisibleColumns(map, fields); + + const headers = columns.map((col) => col.header); + + const expectedOrder = [ + 'header6', + 'header3', + 'header1', + 'header4', + 'header2', + 'header5', + 'header7', + 'header8', + 'header9', + 'header10', + ]; + + expect(headers).toStrictEqual(expectedOrder); + + map.header6.excluded = true; + map.header1.excluded = true; + map.header5 = { + excluded: true, + }; + + const filtered = orderAndFilterVisibleColumns(map, fields); + + const headersFiltered = filtered.map((col) => col.header); + + const expectedOrderFiltered = ['header3', 'header4', 'header2', 'header7', 'header8', 'header9', 'header10']; + + expect(headersFiltered).toStrictEqual(expectedOrderFiltered); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts index 74f07ecb9..d831a8fe8 100644 --- a/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts +++ b/packages/codegen-ui/lib/__tests__/generic-from-datastore.test.ts @@ -15,7 +15,12 @@ */ import { getGenericFromDataStore } from '../generic-from-datastore'; import { HasManyRelationshipType } from '../types'; -import { schemaWithEnums, schemaWithNonModels, schemaWithRelationships } from './__utils__/mock-schemas'; +import { + schemaWithEnums, + schemaWithNonModels, + schemaWithRelationships, + schemaWithAssumptions, +} from './__utils__/mock-schemas'; describe('getGenericFromDataStore', () => { it('should map fields', () => { @@ -118,4 +123,21 @@ describe('getGenericFromDataStore', () => { Misc: { fields: { quotes: { dataType: 'String', required: false, readOnly: false, isArray: true } } }, }); }); + + it('should handle schema with assumed associated fields and modldkjld', () => { + const genericSchema = getGenericFromDataStore(schemaWithAssumptions); + const userFields = genericSchema.models.User.fields; + + expect(userFields.friends.relationship).toStrictEqual({ + type: 'HAS_MANY', + relatedModelName: 'Friend', + relatedModelField: 'friendId', + }); + + expect(userFields.posts.relationship).toStrictEqual({ + type: 'HAS_MANY', + relatedModelName: 'Post', + relatedModelField: 'userPostsId', + }); + }); }); diff --git a/packages/codegen-ui/lib/__tests__/string-formatter.test.ts b/packages/codegen-ui/lib/__tests__/string-formatter.test.ts index a35bbdceb..d15b1fa8b 100644 --- a/packages/codegen-ui/lib/__tests__/string-formatter.test.ts +++ b/packages/codegen-ui/lib/__tests__/string-formatter.test.ts @@ -13,25 +13,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DateTimeFormat, DATE, TIME } from '../types'; -import { formatDate, formatTime, formatDateTime } from '../utils/string-formatter'; +import { NonLocaleDateTimeFormat, DATE, TIME } from '../types/string-format'; +import { formatter } from '../utils/string-formatter'; describe('string-formatter tests', () => { describe('Date and/or time formatting', () => { test('Date formats', () => { const awsDate = '2020-02-29'; - expect(formatDate(awsDate, 'locale')).toBe(new Date('2020-02-29').toLocaleDateString()); - expect(formatDate(awsDate, 'YYYY.MM.DD')).toBe('2020.02.29'); - expect(formatDate(awsDate, 'DD.MM.YYYY')).toBe('29.02.2020'); - expect(formatDate(awsDate, 'MM/DD/YYYY')).toBe('02/29/2020'); - expect(formatDate(awsDate, 'Mmm DD, YYYY')).toBe('Feb 29, 2020'); + expect(formatter(awsDate, { type: 'DateFormat', format: { dateFormat: 'locale' } })).toBe( + new Date('2020-02-29').toLocaleDateString(), + ); + expect(formatter(awsDate, { type: 'DateFormat', format: { dateFormat: 'YYYY.MM.DD' } })).toBe('2020.02.29'); + expect(formatter(awsDate, { type: 'DateFormat', format: { dateFormat: 'DD.MM.YYYY' } })).toBe('29.02.2020'); + expect(formatter(awsDate, { type: 'DateFormat', format: { dateFormat: 'MM/DD/YYYY' } })).toBe('02/29/2020'); + expect(formatter(awsDate, { type: 'DateFormat', format: { dateFormat: 'Mmm DD, YYYY' } })).toBe('Feb 29, 2020'); const invalidDate = 'Not a date'; - expect(formatDate(invalidDate, 'Mmm DD, YYYY')).toBe(invalidDate); + expect(formatter(invalidDate, { type: 'DateFormat', format: { dateFormat: 'Mmm DD, YYYY' } })).toBe(invalidDate); const nullish = undefined; - expect(formatDate(nullish as any, DATE.DMY)).toBe(nullish); + expect(formatter(nullish as any, { type: 'DateFormat', format: { dateFormat: DATE.DMY } })).toBe(nullish); }); test('Time formats', () => { @@ -42,15 +44,17 @@ describe('string-formatter tests', () => { date.setMinutes(45); date.setSeconds(23, 222); - expect(formatTime(awsTime, 'locale')).toBe(date.toLocaleTimeString()); - expect(formatTime(awsTime, 'hours12')).toBe('3:45:23 PM'); - expect(formatTime(awsTime, 'hours24')).toBe('15:45:23'); + expect(formatter(awsTime, { type: 'TimeFormat', format: { timeFormat: 'locale' } })).toBe( + date.toLocaleTimeString(), + ); + expect(formatter(awsTime, { type: 'TimeFormat', format: { timeFormat: 'hours12' } })).toBe('3:45:23 PM'); + expect(formatter(awsTime, { type: 'TimeFormat', format: { timeFormat: 'hours24' } })).toBe('15:45:23'); const invalidTime = 'Not:time:'; - expect(formatTime(invalidTime, 'locale')).toBe(invalidTime); + expect(formatter(invalidTime, { type: 'TimeFormat', format: { timeFormat: 'locale' } })).toBe(invalidTime); const nullish = undefined; - expect(formatTime(nullish as any, TIME.HOURS_24)).toBe(nullish); + expect(formatter(nullish as any, { type: 'TimeFormat', format: { timeFormat: TIME.HOURS_24 } })).toBe(nullish); }); test('Datetime formats', () => { @@ -58,24 +62,56 @@ describe('string-formatter tests', () => { const localDateStr = new Date(Date.parse(awsDateTime)); - const mixedFormatting: DateTimeFormat = { - dateTimeFormat: { + const mixedFormatting: NonLocaleDateTimeFormat = { + nonLocaleDateTimeFormat: { dateFormat: DATE.DMY, timeFormat: TIME.HOURS_12, }, }; - expect(formatDateTime(awsDateTime, 'locale')).toBe(localDateStr.toLocaleString()); - expect(formatDateTime(awsDateTime, mixedFormatting.dateTimeFormat)).toBe('29.02.2020 - 3:45:23 PM'); + expect(formatter(awsDateTime, { type: 'LocaleDateTimeFormat', format: { localeDateTimeFormat: 'locale' } })).toBe( + localDateStr.toLocaleString(), + ); + expect( + formatter(awsDateTime, { + type: 'NonLocaleDateTimeFormat', + format: { nonLocaleDateTimeFormat: mixedFormatting.nonLocaleDateTimeFormat }, + }), + ).toBe('29.02.2020 - 3:45:23 PM'); const invalidDateTime = 'Not a valid datetime'; - expect(formatDateTime(invalidDateTime, 'locale')).toBe(invalidDateTime); + expect( + formatter(invalidDateTime, { type: 'LocaleDateTimeFormat', format: { localeDateTimeFormat: 'locale' } }), + ).toBe(invalidDateTime); const awsTimeStamp = '1582991123222'; - expect(formatDateTime(awsTimeStamp, mixedFormatting.dateTimeFormat)).toBe('29.02.2020 - 3:45:23 PM'); + expect( + formatter(awsTimeStamp, { + type: 'NonLocaleDateTimeFormat', + format: { nonLocaleDateTimeFormat: mixedFormatting.nonLocaleDateTimeFormat }, + }), + ).toBe('29.02.2020 - 3:45:23 PM'); const nullish = undefined; - expect(formatDateTime(nullish as any, mixedFormatting.dateTimeFormat)).toBe(nullish); + expect( + formatter(nullish as any, { + type: 'NonLocaleDateTimeFormat', + format: { nonLocaleDateTimeFormat: mixedFormatting.nonLocaleDateTimeFormat }, + }), + ).toBe(nullish); + }); + + test('format returns undefined', () => { + const awsDateTime = '2020-02-29T15:45:23.222Z'; + const mixedFormatting: NonLocaleDateTimeFormat = { + nonLocaleDateTimeFormat: { + dateFormat: DATE.DMY, + timeFormat: TIME.HOURS_12, + }, + }; + expect( + formatter(awsDateTime, { type: 'UnknownFormat' as any, format: mixedFormatting.nonLocaleDateTimeFormat }), + ).toBe(awsDateTime); }); }); }); diff --git a/packages/codegen-ui/lib/__tests__/utils/form-component-metadata.test.ts b/packages/codegen-ui/lib/__tests__/utils/form-component-metadata.test.ts new file mode 100644 index 000000000..fe13d9ce6 --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/utils/form-component-metadata.test.ts @@ -0,0 +1,103 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { generateFormDefinition } from '../../generate-form-definition'; +import { getGenericFromDataStore } from '../../generic-from-datastore'; +import { FormDefinition, StudioForm } from '../../types'; +import { mapFormMetadata } from '../../utils/form-component-metadata'; +import { getBasicFormDefinition } from '../__utils__/basic-form-definition'; +import { schemaWithAssumptions } from '../__utils__/mock-schemas'; + +describe('mapFormMetaData', () => { + it('should not map metadata for sectional elements', () => { + const formDefinition: FormDefinition = { + ...getBasicFormDefinition(), + elements: { + myHeading: { componentType: 'Heading', props: { level: 2, children: 'Create a Post' } }, + name: { componentType: 'TextField', props: { label: 'Label' }, studioFormComponentType: 'TextField' }, + myText: { componentType: 'Text', props: { children: 'Did you put your name above?' } }, + myDivider: { componentType: 'Divider', props: { orientation: 'horizontal' } }, + }, + elementMatrix: [['myHeading'], ['name'], ['myText'], ['myDivider']], + }; + + const form: StudioForm = { + name: 'CustomWithSectionalElements', + formActionType: 'create', + dataType: { + dataSourceType: 'Custom', + dataTypeName: 'Post', + }, + fields: { + name: { + inputType: { + type: 'TextField', + }, + }, + }, + sectionalElements: { + myHeading: { + position: { + fixed: 'first', + }, + type: 'Heading', + level: 2, + text: 'Create a Post', + }, + myText: { + position: { + below: 'name', + }, + type: 'Text', + text: 'Did you put your name above?', + }, + myDivider: { + position: { + below: 'myText', + }, + type: 'Divider', + }, + }, + style: {}, + cta: {}, + }; + + const { fieldConfigs } = mapFormMetadata(form, formDefinition); + + expect('name' in fieldConfigs).toBe(true); + expect('myDivider' in fieldConfigs || 'myText' in fieldConfigs || 'myHeading' in fieldConfigs).toBe(false); + }); + it('should map isArray type for autogenerated datastore form', () => { + const dataSchema = getGenericFromDataStore(schemaWithAssumptions); + + const form: StudioForm = { + name: 'DataStoreForm', + formActionType: 'create', + dataType: { + dataSourceType: 'DataStore', + dataTypeName: 'User', + }, + fields: {}, + sectionalElements: {}, + style: {}, + cta: {}, + }; + + const { fieldConfigs } = mapFormMetadata(form, generateFormDefinition({ form, dataSchema }), dataSchema); + + expect('badges' in fieldConfigs).toBe(true); + expect(fieldConfigs.badges.isArray).toBe(true); + }); +}); diff --git a/packages/codegen-ui/lib/__tests__/utils/form-to-component/map-form-definition-to-component.test.ts b/packages/codegen-ui/lib/__tests__/utils/form-to-component/map-form-definition-to-component.test.ts new file mode 100644 index 000000000..cd8e843f0 --- /dev/null +++ b/packages/codegen-ui/lib/__tests__/utils/form-to-component/map-form-definition-to-component.test.ts @@ -0,0 +1,91 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { FormDefinition } from '../../../types'; +import { mapFormDefinitionToComponent } from '../../../utils/form-to-component'; +import { getBasicFormDefinition } from '../../__utils__/basic-form-definition'; + +describe('mapFormDefinitionToComponent', () => { + it('should map options for RadioGroupField', () => { + const formDefinition: FormDefinition = { + ...getBasicFormDefinition(), + elements: { + city: { + componentType: 'RadioGroupField', + props: { label: 'City', name: 'city' }, + valueMappings: { + bindingProperties: {}, + values: [ + { value: { value: 'NEW_YORK' }, displayValue: { value: 'New York' } }, + { value: { value: 'SAN_FRANCISCO' } }, + ], + }, + }, + }, + elementMatrix: [['city']], + }; + + const radioGroupField = mapFormDefinitionToComponent('CreateDog', formDefinition).children?.[0]; + + expect(radioGroupField?.children).toStrictEqual([ + { + name: 'cityRadio0', + componentType: 'Radio', + properties: { children: { value: 'New York' }, value: { value: 'NEW_YORK' } }, + }, + { + name: 'cityRadio1', + componentType: 'Radio', + properties: { children: { value: 'SAN_FRANCISCO' }, value: { value: 'SAN_FRANCISCO' } }, + }, + ]); + }); + + it('should map options for SelectField', () => { + const formDefinition: FormDefinition = { + ...getBasicFormDefinition(), + elements: { + city: { + componentType: 'SelectField', + props: { label: 'City' }, + defaultValue: 'NEW_YORK', + valueMappings: { + bindingProperties: {}, + values: [ + { value: { value: 'NEW_YORK' }, displayValue: { value: 'New York' } }, + { value: { value: 'SAN_FRANCISCO' } }, + ], + }, + }, + }, + elementMatrix: [['city']], + }; + + const selectField = mapFormDefinitionToComponent('CreateDog', formDefinition).children?.[0]; + + expect(selectField?.children).toStrictEqual([ + { + name: 'cityoption0', + componentType: 'option', + properties: { children: { value: 'New York' }, value: { value: 'NEW_YORK' }, selected: { value: true } }, + }, + { + name: 'cityoption1', + componentType: 'option', + properties: { children: { value: 'SAN_FRANCISCO' }, value: { value: 'SAN_FRANCISCO' } }, + }, + ]); + }); +}); diff --git a/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts b/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts deleted file mode 100644 index feaaaf972..000000000 --- a/packages/codegen-ui/lib/generate-form-definition/form-to-component.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import type { SchemaModel, ModelFields } from '@aws-amplify/datastore'; -import { - StudioComponent, - StudioForm, - StudioComponentChild, - StudioComponentProperty, - DataStoreCreateItemAction, - DataStoreUpdateItemAction, - FixedStudioComponentProperty, -} from '../types'; - -/** - * TODO: remove once form builder depends more on generic data schema - */ -const isGraphQLScalarType = (obj: any): boolean => { - return obj && typeof obj !== 'object'; -}; - -// map the datastore schema fields into form fields -export const mapFieldsToForm = (fields: ModelFields) => { - const formFields: StudioComponentChild[] = []; - Object.entries(fields).forEach(([fieldName, fieldValue]) => { - // TODO: expand studio component child to also support other non text fields - if (isGraphQLScalarType(fieldValue.type) && !fieldValue.isArray) { - formFields.push({ - name: `${fieldName}Field`, - componentType: 'TextField', - properties: { - name: { - value: fieldName, - }, - label: { - value: fieldName, - }, - placeholder: { - value: `${fieldValue.type}`, - }, - ...(fieldValue.isRequired && { - required: { - value: 'true', - type: 'boolean', - }, - }), - }, - }); - } - }); - - return formFields; -}; - -export const mapParentGrid = (name: string, children: StudioComponentChild[] = []): StudioComponentChild => { - return { - name: `${name}Grid`, - componentType: 'Grid', - properties: { - columnGap: { - value: '1rem', - }, - rowGap: { - value: '1rem', - }, - }, - children, - }; -}; - -export const ctaButtonConfig = (): StudioComponentChild => { - return { - name: 'CTAFlex', - componentType: 'Flex', - properties: { - justifyContent: { - value: 'space-between', - }, - marginTop: { - value: '1rem', - }, - }, - children: [ - { - componentType: 'Button', - name: 'CancelButton', - properties: { - label: { - value: 'Cancel', - }, - type: { - value: 'button', - }, - }, - }, - { - componentType: 'Flex', - name: 'SubmitAndResetFlex', - properties: {}, - children: [ - { - componentType: 'Button', - name: 'ClearButton', - properties: { - label: { - value: 'Clear', - }, - type: { - value: 'reset', - }, - }, - }, - { - componentType: 'Button', - name: 'onSubmitDataStore', - properties: { - label: { - value: 'Submit', - }, - type: { - value: 'submit', - }, - variation: { - value: 'primary', - }, - }, - }, - ], - }, - ], - }; -}; - -export const mapOnSubmitEvent = ( - form: StudioForm, - childrenFormFields: StudioComponentChild[], -): DataStoreCreateItemAction | DataStoreUpdateItemAction => { - if (form.formActionType === 'create') { - return { - action: 'Amplify.DataStoreCreateItemAction', - parameters: { - model: form.dataType.dataTypeName, - fields: childrenFormFields.reduce( - (prev: { [propertyName: string]: StudioComponentProperty }, { name, properties }) => { - return { - ...prev, - [(properties.name as any).value]: { - componentName: name, - property: 'value', - }, - }; - }, - {}, - ), - }, - } as DataStoreCreateItemAction; - } - /** - * TODO: Read DataStore Spec to find CustomPrimaryKey if not ID - */ - const { value: primaryKey } = childrenFormFields.find( - ({ properties }) => (properties.name as FixedStudioComponentProperty).value === 'id', - )?.properties.name as FixedStudioComponentProperty; - return { - action: 'Amplify.DataStoreUpdateItemAction', - parameters: { - model: form.dataType.dataTypeName, - id: { - value: primaryKey || 'id', - }, - }, - } as DataStoreUpdateItemAction; -}; -/** - * TODO to be removed when form builder depends on generic data schema - */ -export const mapFormToComponent = (form: StudioForm, dataSchema: SchemaModel): StudioComponent => { - // here we can merge the datastore schema with the form - // right now it's only creating fields from the existing datastore schema - // TODO: manage merging fields from form and datastore - const childrenFormFields = mapFieldsToForm(dataSchema.fields); - - const component: StudioComponent = { - name: form.name, - properties: {}, - bindingProperties: { - onCancel: { type: 'Event' }, - }, - events: { - onSubmit: mapOnSubmitEvent(form, childrenFormFields), - }, - // codegen will default to rendering the component with this name - componentType: 'form', - children: [mapParentGrid(form.name, childrenFormFields), ctaButtonConfig()], - }; - - return component; -}; diff --git a/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts b/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts index e791ef349..1ea15244e 100644 --- a/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts +++ b/packages/codegen-ui/lib/generate-form-definition/generate-form-definition.ts @@ -14,8 +14,9 @@ limitations under the License. */ -import { addDataStoreModelField, mapElementMatrix, mapStyles, mapElements } from './helpers'; -import { StudioForm, DataStoreModelField, FormDefinition, ModelFieldsConfigs, StudioFieldPosition } from '../types'; +import { mapModelFieldsConfigs, mapElementMatrix, mapStyles, mapElements, mapButtons } from './helpers'; +import { StudioForm, FormDefinition, ModelFieldsConfigs, StudioFieldPosition, GenericDataSchema } from '../types'; +import { InvalidInputError } from '../errors'; /** * Helper that turns the StudioForm model into definition that can be used to render @@ -26,22 +27,34 @@ import { StudioForm, DataStoreModelField, FormDefinition, ModelFieldsConfigs, St */ export function generateFormDefinition({ form, - modelInfo, + dataSchema, }: { form: StudioForm; - modelInfo?: { fields: DataStoreModelField[] }; + dataSchema?: GenericDataSchema; }): FormDefinition { const formDefinition: FormDefinition = { - form: { layoutStyle: {} }, + form: { layoutStyle: mapStyles(form.style) }, elements: {}, - buttons: {}, + buttons: { + buttonConfigs: {}, + position: '', + buttonMatrix: [], + }, elementMatrix: [], }; - const modelFieldsConfigs: ModelFieldsConfigs = {}; - if (modelInfo) { - modelInfo.fields.forEach((field) => { - addDataStoreModelField(formDefinition, modelFieldsConfigs, field); + let modelFieldsConfigs: ModelFieldsConfigs = {}; + + if (form.dataType.dataSourceType !== 'Custom') { + if (!dataSchema) { + throw new InvalidInputError( + `Data schema is missing for form of data source type ${form.dataType.dataSourceType}`, + ); + } + modelFieldsConfigs = mapModelFieldsConfigs({ + dataSchema, + formDefinition, + dataTypeName: form.dataType.dataTypeName, }); } @@ -65,7 +78,7 @@ export function generateFormDefinition({ mapElements({ form, formDefinition, modelFieldsConfigs }); - formDefinition.form.layoutStyle = mapStyles(form.style); + formDefinition.buttons = mapButtons(form.cta); return formDefinition; } diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts deleted file mode 100644 index e141aa73f..000000000 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/datastore-model.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { DataStoreModelField, FormDefinition, ModelFieldsConfigs } from '../../types'; -import { FIELD_TYPE_MAP } from './field-type-map'; -import { InvalidInputError } from '../../errors'; - -/** - * Impure function that adds fields from DataStore to temporary util object, modelFieldsConfigs - * and to the formDefinition - */ -/* eslint-disable no-param-reassign */ -export function addDataStoreModelField( - formDefinition: FormDefinition, - modelFieldsConfigs: ModelFieldsConfigs, - field: DataStoreModelField, -) { - if (field.isArray) { - throw new InvalidInputError('Array types are not yet supported'); - } - - const dataType = typeof field.type === 'string' ? field.type : Object.keys(field.type)[0]; - const defaultComponent = FIELD_TYPE_MAP[dataType]?.defaultComponent; - - if (!defaultComponent) { - throw new InvalidInputError('Field type could not be mapped to a component'); - } - - const isAutoExcludedField = field.isReadOnly || (field.name === 'id' && field.type === 'ID' && field.isRequired); - - if (!isAutoExcludedField) { - formDefinition.elementMatrix.push([field.name]); - } - - // TODO: map Enums to valueMappings - modelFieldsConfigs[field.name] = { - label: field.name, - inputType: { - type: defaultComponent, - required: field.isRequired, - readOnly: field.isReadOnly, - name: field.name, - value: 'true', - }, - }; -} -/* eslint-enable no-param-reassign */ diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/defaults.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/defaults.ts index 66242858c..ba1b4f312 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/defaults.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/defaults.ts @@ -31,13 +31,28 @@ export const FORM_DEFINITION_DEFAULTS = { inputType: { label: 'Label', defaultCountryCode: '+1', - value: 'true', + value: 'fieldName', name: 'fieldName', - valueMappings: [{ value: 'value', displayValue: 'Label' }], + valueMappings: { values: [{ value: { value: 'Option' } }] }, }, + radioGroupFieldBooleanDisplayValue: { true: 'Yes', false: 'No' }, }, sectionalElement: { text: 'text', }, + + cta: { + position: 'bottom', + buttonMatrix: [['clear'], ['cancel', 'submit']], + cancel: { + label: 'Cancel', + }, + clear: { + label: 'Clear', + }, + submit: { + label: 'Submit', + }, + }, }; diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/field-type-map.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/field-type-map.ts index 147318fef..9a50b8e34 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/field-type-map.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/field-type-map.ts @@ -14,15 +14,15 @@ limitations under the License. */ -import { FormDefinitionInputElement } from '../../types'; +import { FieldTypeMapKeys, FormInputType } from '../../types'; /** * Maps data types to UI Components */ export const FIELD_TYPE_MAP: { - [key: string]: { - defaultComponent: FormDefinitionInputElement['componentType']; - supportedComponents: Set; + [key in FieldTypeMapKeys]: { + defaultComponent: FormInputType; + supportedComponents: Set; }; } = { ID: { @@ -34,48 +34,48 @@ export const FIELD_TYPE_MAP: { supportedComponents: new Set(['TextAreaField', 'TextField', 'PasswordField']), }, Int: { - defaultComponent: 'TextField', - supportedComponents: new Set(['SliderField', 'StepperField', 'TextField']), + defaultComponent: 'NumberField', + supportedComponents: new Set(['SliderField', 'StepperField', 'NumberField']), }, Float: { - defaultComponent: 'TextField', - supportedComponents: new Set(['SliderField', 'StepperField', 'TextField']), + defaultComponent: 'NumberField', + supportedComponents: new Set(['SliderField', 'StepperField', 'NumberField']), }, AWSDate: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'DateField', + supportedComponents: new Set(['DateField']), }, AWSTime: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'TimeField', + supportedComponents: new Set(['TimeField']), }, AWSDateTime: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'DateTimeField', + supportedComponents: new Set(['DateTimeField']), }, AWSTimestamp: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'DateTimeField', + supportedComponents: new Set(['DateTimeField']), }, AWSEmail: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'EmailField', + supportedComponents: new Set(['EmailField']), }, AWSURL: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'URLField', + supportedComponents: new Set(['URLField']), }, AWSIPAddress: { - defaultComponent: 'TextField', - supportedComponents: new Set(['TextField']), + defaultComponent: 'IPAddressField', + supportedComponents: new Set(['IPAddressField']), }, Boolean: { defaultComponent: 'SwitchField', supportedComponents: new Set(['ToggleButton', 'CheckboxField', 'RadioGroupField', 'SwitchField']), }, AWSJSON: { - defaultComponent: 'TextAreaField', - supportedComponents: new Set(['TextField', 'TextAreaField']), + defaultComponent: 'JSONField', + supportedComponents: new Set(['TextField', 'JSONField']), }, AWSPhone: { defaultComponent: 'PhoneNumberField', @@ -89,4 +89,8 @@ export const FIELD_TYPE_MAP: { defaultComponent: 'SelectField', supportedComponents: new Set(['SelectField']), }, + NonModel: { + defaultComponent: 'TextAreaField', + supportedComponents: new Set(['TextAreaField']), + }, }; diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts index 980715e59..3c5292118 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/form-field.ts @@ -19,17 +19,123 @@ import { StudioGenericFieldConfig, ModelFieldsConfigs, StudioFormFieldConfig, + StudioFormValueMappings, + FieldValidationConfiguration, + ValidationTypes, } from '../../types'; import { InternalError, InvalidInputError } from '../../errors'; import { FORM_DEFINITION_DEFAULTS } from './defaults'; import { deleteUndefined, getFirstDefinedValue, getFirstNumber, getFirstString } from './mapper-utils'; -function getOptionsFromValueMappings( - valueMappings: { displayValue: string; value: string }[], -): { value: string; children: string }[] { - return valueMappings.map(({ displayValue, value }) => { - return { value, children: displayValue }; +export function mergeValueMappings( + base?: StudioFormValueMappings, + override?: StudioFormValueMappings, +): StudioFormValueMappings { + let values: StudioFormValueMappings['values'] = []; + + if (!base && override) { + values = override.values; + } else if (base && !override) { + values = base.values; + } else if (base && override) { + const overrideMap = new Map( + override.values.map(({ displayValue, value }) => [JSON.stringify(value), displayValue]), + ); + values = base.values.map(({ displayValue, value }) => { + const stringifiedBaseValue = JSON.stringify(value); + const overrideDisplayValue = overrideMap.get(stringifiedBaseValue); + if (overrideDisplayValue) { + return { displayValue: overrideDisplayValue, value }; + } + return { displayValue, value }; + }); + } + + return { + values, + bindingProperties: { ...base?.bindingProperties, ...override?.bindingProperties }, + }; +} + +function getRadioGroupFieldValueMappings( + config: StudioGenericFieldConfig, + baseConfig?: StudioGenericFieldConfig, +): StudioFormValueMappings { + const valueMappings: StudioFormValueMappings = + baseConfig?.inputType?.valueMappings?.values.length || config?.inputType?.valueMappings?.values.length + ? mergeValueMappings(baseConfig?.inputType?.valueMappings, config.inputType?.valueMappings) + : FORM_DEFINITION_DEFAULTS.field.inputType.valueMappings; + + const dataType = config.dataType ?? baseConfig?.dataType; + if (dataType === 'Boolean') { + const trueOverride = valueMappings.values.find( + ({ value }) => 'value' in value && value.value === 'true', + )?.displayValue; + const falseOverride = valueMappings.values.find( + ({ value }) => 'value' in value && value.value === 'false', + )?.displayValue; + + const { + field: { + radioGroupFieldBooleanDisplayValue: { true: trueDefault, false: falseDefault }, + }, + } = FORM_DEFINITION_DEFAULTS; + return { + values: [ + { value: { value: 'true' }, displayValue: trueOverride ?? { value: trueDefault } }, + { value: { value: 'false' }, displayValue: falseOverride ?? { value: falseDefault } }, + ], + }; + } + + return valueMappings; +} + +// pure function that merges in validations in param with defaults +function getMergedValidations( + componentType: string, + validations: (FieldValidationConfiguration[] | undefined)[], + isRequired?: boolean, +): (FieldValidationConfiguration & { immutable?: true })[] | undefined { + const mergedValidations: (FieldValidationConfiguration & { immutable?: true })[] = []; + + if (isRequired) { + mergedValidations.push({ type: ValidationTypes.REQUIRED, immutable: true }); + } + + const ComponentTypeToDefaultValidations: { + [componentType: string]: (FieldValidationConfiguration & { immutable: true })[]; + } = { + IPAddressField: [{ type: ValidationTypes.IP_ADDRESS, immutable: true }], + URLField: [{ type: ValidationTypes.URL, immutable: true }], + EmailField: [{ type: ValidationTypes.EMAIL, immutable: true }], + JSONField: [{ type: ValidationTypes.JSON, immutable: true }], + PhoneNumberField: [{ type: ValidationTypes.PHONE, immutable: true }], + }; + + const defaultValidation = ComponentTypeToDefaultValidations[componentType]; + + if (defaultValidation) { + mergedValidations.push(...defaultValidation); + } + + validations.forEach((validationArray) => { + if (validationArray) { + mergedValidations.push(...validationArray); + } }); + + return mergedValidations.length ? mergedValidations : undefined; +} + +function getTextFieldType(componentType: string): string | undefined { + const ComponentToTypeMap: { [key: string]: string } = { + NumberField: 'number', + DateField: 'date', + TimeField: 'time', + DateTimeField: 'datetime-local', + }; + return ComponentToTypeMap[componentType]; } /** @@ -45,20 +151,30 @@ export function getFormDefinitionInputElement( if (!componentType) { throw new InvalidInputError('Field config is missing input type'); } - + const defaultStringValue = getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]); + const isRequiredValue = getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]); let formDefinitionElement: FormDefinitionInputElement; switch (componentType) { case 'TextField': + case 'NumberField': + case 'DateField': + case 'TimeField': + case 'DateTimeField': + case 'IPAddressField': + case 'URLField': + case 'EmailField': formDefinitionElement = { componentType: 'TextField', props: { label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder, - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + defaultValue: defaultStringValue, + type: getTextFieldType(componentType), }, + studioFormComponentType: componentType, }; break; case 'SwitchField': @@ -66,12 +182,9 @@ export function getFormDefinitionInputElement( componentType: 'SwitchField', props: { label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, - defaultChecked: getFirstDefinedValue([ - config.inputType?.defaultChecked, - baseConfig?.inputType?.defaultChecked, - ]), - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), - isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), + defaultChecked: + getFirstDefinedValue([config.inputType?.defaultChecked, baseConfig?.inputType?.defaultChecked]) || false, + isDisabled: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), }, }; @@ -86,11 +199,11 @@ export function getFormDefinitionInputElement( config.inputType?.defaultCountryCode || baseConfig?.inputType?.defaultCountryCode || FORM_DEFINITION_DEFAULTS.field.inputType.defaultCountryCode, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder, - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + defaultValue: defaultStringValue, }, }; break; @@ -101,27 +214,28 @@ export function getFormDefinitionInputElement( props: { label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder, + placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder || 'Please select an option', isDisabled: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), }, - options: getOptionsFromValueMappings( - config.inputType?.valueMappings || baseConfig?.inputType?.valueMappings || [], - ), - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + + defaultValue: defaultStringValue, + valueMappings: mergeValueMappings(baseConfig?.inputType?.valueMappings, config.inputType?.valueMappings), }; break; case 'TextAreaField': + case 'JSONField': formDefinitionElement = { componentType: 'TextAreaField', props: { label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder, - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + defaultValue: defaultStringValue, }, + studioFormComponentType: componentType, }; break; @@ -136,7 +250,7 @@ export function getFormDefinitionInputElement( isDisabled: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), defaultValue: getFirstNumber([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, }, }; break; @@ -152,7 +266,7 @@ export function getFormDefinitionInputElement( isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), defaultValue: getFirstNumber([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, }, }; @@ -164,10 +278,8 @@ export function getFormDefinitionInputElement( props: { children: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, isDisabled: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), - defaultPressed: getFirstDefinedValue([ - config.inputType?.defaultChecked, - baseConfig?.inputType?.defaultChecked, - ]), + defaultPressed: + getFirstDefinedValue([config.inputType?.defaultChecked, baseConfig?.inputType?.defaultChecked]) || false, }, }; break; @@ -181,10 +293,8 @@ export function getFormDefinitionInputElement( value: config.inputType?.value || baseConfig?.inputType?.value || FORM_DEFINITION_DEFAULTS.field.inputType.value, isDisabled: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), - defaultChecked: getFirstDefinedValue([ - config.inputType?.defaultChecked, - baseConfig?.inputType?.defaultChecked, - ]), + defaultChecked: + getFirstDefinedValue([config.inputType?.defaultChecked, baseConfig?.inputType?.defaultChecked]) || false, }, }; break; @@ -196,15 +306,11 @@ export function getFormDefinitionInputElement( label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, name: config.inputType?.name || baseConfig?.inputType?.name || FORM_DEFINITION_DEFAULTS.field.inputType.name, isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + defaultValue: defaultStringValue, descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, }, - radios: getOptionsFromValueMappings( - config.inputType?.valueMappings || - baseConfig?.inputType?.valueMappings || - FORM_DEFINITION_DEFAULTS.field.inputType.valueMappings, - ), + valueMappings: getRadioGroupFieldValueMappings(config, baseConfig), }; break; @@ -214,18 +320,26 @@ export function getFormDefinitionInputElement( props: { label: config.label || baseConfig?.label || FORM_DEFINITION_DEFAULTS.field.inputType.label, descriptiveText: config.inputType?.descriptiveText ?? baseConfig?.inputType?.descriptiveText, - isRequired: getFirstDefinedValue([config.inputType?.required, baseConfig?.inputType?.required]), + isRequired: isRequiredValue, isReadOnly: getFirstDefinedValue([config.inputType?.readOnly, baseConfig?.inputType?.readOnly]), placeholder: config.inputType?.placeholder || baseConfig?.inputType?.placeholder, - defaultValue: getFirstString([config.inputType?.defaultValue, baseConfig?.inputType?.defaultValue]), + defaultValue: defaultStringValue, }, }; break; - default: throw new InvalidInputError(`componentType ${componentType} could not be mapped`); } + const mergedValidations = getMergedValidations( + componentType, + [baseConfig?.validations, config?.validations], + isRequiredValue, + ); + + formDefinitionElement.validations = mergedValidations; + formDefinitionElement.dataType = config?.dataType || baseConfig?.dataType; + deleteUndefined(formDefinitionElement); deleteUndefined(formDefinitionElement.props); diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/index.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/index.ts index daebfaeca..616989368 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/index.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/index.ts @@ -13,10 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export * from './datastore-model'; +export * from './model-fields-configs'; export * from './position'; export * from './form-field'; export * from './sectional-element'; export * from './field-type-map'; export * from './map-element'; export * from './map-styles'; +export * from './map-cta'; diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/map-cta.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/map-cta.ts new file mode 100644 index 000000000..3b859e444 --- /dev/null +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/map-cta.ts @@ -0,0 +1,77 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { FORM_DEFINITION_DEFAULTS } from './defaults'; +import { StudioFormCTA, ButtonConfig, StudioFormCTAButton, FormDefinitionButtonElement } from '../../types'; + +function getButtonElement( + key: 'cancel' | 'clear' | 'submit', + override?: StudioFormCTAButton, +): FormDefinitionButtonElement | undefined { + if (override && 'excluded' in override) { + return undefined; + } + + const ButtonMap = { + cancel: { + name: 'CancelButton', + type: 'button', + }, + clear: { + name: 'ClearButton', + type: 'reset', + }, + submit: { + name: 'SubmitButton', + type: 'submit', + variation: 'primary', + }, + }; + + const { cta: defaults } = FORM_DEFINITION_DEFAULTS; + + const element: FormDefinitionButtonElement = { + name: ButtonMap[key].name, + componentType: 'Button', + props: { + children: override?.children ?? defaults[key].label, + type: ButtonMap[key].type, + }, + }; + + if (key === 'submit') { + element.props.variation = ButtonMap[key].variation; + } + + return element; +} + +export function mapButtons(buttons?: StudioFormCTA): ButtonConfig { + const defaults = FORM_DEFINITION_DEFAULTS.cta; + + const buttonMapping: ButtonConfig = { + position: buttons?.position ?? defaults.position, + buttonMatrix: defaults.buttonMatrix, + buttonConfigs: {}, + }; + + const keys: (keyof ButtonConfig['buttonConfigs'])[] = ['submit', 'cancel', 'clear']; + + keys.forEach((key) => { + buttonMapping.buttonConfigs[key] = getButtonElement(key, buttons?.[key]); + }); + + return buttonMapping; +} diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/map-styles.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/map-styles.ts index 83183a2a4..cf0b39af3 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/map-styles.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/map-styles.ts @@ -13,14 +13,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StudioFormStyle } from '../../types'; +import { StudioFormStyle, FormDefinition } from '../../types'; import { FORM_DEFINITION_DEFAULTS } from './defaults'; -function hasValue(config: { tokenReference?: string; value?: string } | undefined): boolean { +function hasValue( + config: { tokenReference?: string; value?: string } | undefined, +): config is { tokenReference: string } | { value: string } { return !!(config && (config.tokenReference || config.value)); } -export function mapStyles(styles: StudioFormStyle): StudioFormStyle { +export function mapStyles(styles: StudioFormStyle): FormDefinition['form']['layoutStyle'] { const defaults = FORM_DEFINITION_DEFAULTS.styles; return { horizontalGap: hasValue(styles.horizontalGap) ? styles.horizontalGap : defaults.horizontalGap, diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts new file mode 100644 index 000000000..a39507af6 --- /dev/null +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/model-fields-configs.ts @@ -0,0 +1,121 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { sentenceCase } from 'change-case'; + +import { InvalidInputError } from '../../errors'; +import { + FieldTypeMapKeys, + FormDefinition, + GenericDataField, + GenericDataSchema, + ModelFieldsConfigs, + StudioFieldInputConfig, + StudioGenericFieldConfig, +} from '../../types'; +import { FIELD_TYPE_MAP } from './field-type-map'; + +export function getFieldTypeMapKey(field: GenericDataField): FieldTypeMapKeys { + if (typeof field.dataType === 'object' && 'enum' in field.dataType) { + return 'Enum'; + } + + if ((typeof field.dataType === 'object' && 'model' in field.dataType) || field.relationship?.relatedModelName) { + return 'Relationship'; + } + + if (typeof field.dataType === 'object' && 'nonModel' in field.dataType) { + return 'NonModel'; + } + return field.dataType; +} + +export function getFieldConfigFromModelField({ + fieldName, + field, + dataSchema, +}: { + fieldName: string; + field: GenericDataField; + dataSchema: GenericDataSchema; +}): StudioGenericFieldConfig { + const fieldTypeMapKey = getFieldTypeMapKey(field); + + const { defaultComponent } = FIELD_TYPE_MAP[fieldTypeMapKey]; + + const config: StudioGenericFieldConfig & { inputType: StudioFieldInputConfig } = { + label: sentenceCase(fieldName), + dataType: field.dataType, + inputType: { + type: defaultComponent, + required: field.required, + readOnly: field.readOnly, + name: fieldName, + value: fieldName, + isArray: field.isArray, + }, + }; + + if (typeof field.dataType === 'object' && 'enum' in field.dataType) { + const fieldEnums = dataSchema.enums[field.dataType.enum]; + if (!fieldEnums) { + throw new InvalidInputError(`Values could not be found for enum ${field.dataType.enum}`); + } + + config.inputType.valueMappings = { + values: fieldEnums.values.map((value) => ({ + displayValue: { value: sentenceCase(value) ? sentenceCase(value) : value }, + value: { value }, + })), + }; + } + + return config; +} + +/** + * Impure function that adds fields from DataStore to temporary util object, modelFieldsConfigs + * and to the formDefinition + */ +export function mapModelFieldsConfigs({ + dataTypeName, + formDefinition, + dataSchema, +}: { + dataTypeName: string; + dataSchema: GenericDataSchema; + formDefinition: FormDefinition; +}) { + const modelFieldsConfigs: ModelFieldsConfigs = {}; + const model = dataSchema.models[dataTypeName]; + + if (!model) { + throw new InvalidInputError(`Model ${dataTypeName} not found`); + } + + Object.entries(model.fields).forEach(([fieldName, field]) => { + const isAutoExcludedField = + field.readOnly || (fieldName === 'id' && field.dataType === 'ID' && field.required) || field.relationship; + + if (!isAutoExcludedField) { + formDefinition.elementMatrix.push([fieldName]); + } + + modelFieldsConfigs[fieldName] = getFieldConfigFromModelField({ fieldName, field, dataSchema }); + }); + + return modelFieldsConfigs; +} diff --git a/packages/codegen-ui/lib/generate-form-definition/helpers/position.ts b/packages/codegen-ui/lib/generate-form-definition/helpers/position.ts index 20361b97b..00335726e 100644 --- a/packages/codegen-ui/lib/generate-form-definition/helpers/position.ts +++ b/packages/codegen-ui/lib/generate-form-definition/helpers/position.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { InvalidInputError } from '../../errors'; import { FormDefinition, StudioFieldPosition } from '../../types'; /** @@ -38,6 +39,13 @@ export function removeAndReturnItemOnward(indices: [number, number], formDefinit return row.splice(indices[1], row.length - indices[1]); } +// helper that throws if every element traversed but none mapped +function throwIfUnmappable(originalQueue: unknown[], requeued: unknown[]) { + if (originalQueue.length === requeued.length) { + throw new InvalidInputError('Unmappable element positions in form'); + } +} + /* eslint-disable no-param-reassign */ /** @@ -51,67 +59,78 @@ export function mapElementMatrix({ formDefinition: FormDefinition; elementQueue: { name: string; position?: StudioFieldPosition; excluded?: boolean }[]; }): void { - const rightOfElementQueue: typeof elementQueue = []; + let belowElementQueue: typeof elementQueue = [...elementQueue]; + let rightOfElementQueue: typeof elementQueue = []; // map elements with no position; position below; position fixed to first - while (elementQueue.length) { - const element = elementQueue.shift(); - if (!element) { - break; - } - if (element.excluded) { - const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + while (belowElementQueue.length) { + const requeued: typeof elementQueue = []; + const tempRightOf: typeof elementQueue = []; - if (previousIndices) { - removeFromMatrix(previousIndices, formDefinition); - } - } else if (element.position && 'rightOf' in element.position && element.position.rightOf) { - rightOfElementQueue.push(element); - } else if (element.position && 'below' in element.position && element.position.below) { - const relationIndices = findIndices(element.position.below, formDefinition.elementMatrix); - if (!relationIndices) { - elementQueue.push(element); - } else { + belowElementQueue.forEach((element) => { + if (element.excluded) { const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + if (previousIndices) { removeFromMatrix(previousIndices, formDefinition); } - formDefinition.elementMatrix.splice(relationIndices[0] + 1, 0, [element.name]); - } - } else if (element.position && 'fixed' in element.position && element.position.fixed === 'first') { - const previousIndices = findIndices(element.name, formDefinition.elementMatrix); - if (previousIndices) { - removeFromMatrix(previousIndices, formDefinition); - } - formDefinition.elementMatrix.unshift([element.name]); - } else { - const previousIndices = findIndices(element.name, formDefinition.elementMatrix); - if (!previousIndices) { - formDefinition.elementMatrix.push([element.name]); + } else if (element.position && 'rightOf' in element.position && element.position.rightOf) { + tempRightOf.push(element); + } else if (element.position && 'below' in element.position && element.position.below) { + const relationIndices = findIndices(element.position.below, formDefinition.elementMatrix); + if (!relationIndices) { + requeued.push(element); + } else { + const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + if (previousIndices) { + removeFromMatrix(previousIndices, formDefinition); + } + formDefinition.elementMatrix.splice(relationIndices[0] + 1, 0, [element.name]); + } + } else if (element.position && 'fixed' in element.position && element.position.fixed === 'first') { + const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + if (previousIndices) { + removeFromMatrix(previousIndices, formDefinition); + } + formDefinition.elementMatrix.unshift([element.name]); + } else { + const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + if (!previousIndices) { + formDefinition.elementMatrix.push([element.name]); + } } - } + }); + + throwIfUnmappable(belowElementQueue, requeued); + + belowElementQueue = requeued; + rightOfElementQueue.push(...tempRightOf); } // map elements with rightOf position while (rightOfElementQueue.length) { - const element = rightOfElementQueue.shift(); - if (!element) { - break; - } - if (element.position && 'rightOf' in element.position && element.position.rightOf) { - const relationIndices = findIndices(element.position.rightOf, formDefinition.elementMatrix); - if (!relationIndices) { - rightOfElementQueue.push(element); - } else { - const previousIndices = findIndices(element.name, formDefinition.elementMatrix); - if (previousIndices) { - const removedItems = removeAndReturnItemOnward(previousIndices, formDefinition); - formDefinition.elementMatrix[relationIndices[0]].splice(relationIndices[1] + 1, 0, ...removedItems); + const requeued: typeof elementQueue = []; + + rightOfElementQueue.forEach((element) => { + if (element.position && 'rightOf' in element.position && element.position.rightOf) { + const relationIndices = findIndices(element.position.rightOf, formDefinition.elementMatrix); + if (!relationIndices) { + requeued.push(element); } else { - formDefinition.elementMatrix[relationIndices[0]].splice(relationIndices[1] + 1, 0, element.name); + const previousIndices = findIndices(element.name, formDefinition.elementMatrix); + if (previousIndices) { + const removedItems = removeAndReturnItemOnward(previousIndices, formDefinition); + formDefinition.elementMatrix[relationIndices[0]].splice(relationIndices[1] + 1, 0, ...removedItems); + } else { + formDefinition.elementMatrix[relationIndices[0]].splice(relationIndices[1] + 1, 0, element.name); + } } } - } + }); + + throwIfUnmappable(rightOfElementQueue, requeued); + + rightOfElementQueue = requeued; } // filter out empty rows diff --git a/packages/codegen-ui/lib/generate-form-definition/index.ts b/packages/codegen-ui/lib/generate-form-definition/index.ts index d009eba35..29d0758aa 100644 --- a/packages/codegen-ui/lib/generate-form-definition/index.ts +++ b/packages/codegen-ui/lib/generate-form-definition/index.ts @@ -13,6 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export { FIELD_TYPE_MAP, getFormDefinitionInputElement, getFormDefinitionSectionalElement } from './helpers'; +export { + FIELD_TYPE_MAP, + getFormDefinitionInputElement, + getFormDefinitionSectionalElement, + getFieldTypeMapKey, + getFieldConfigFromModelField, +} from './helpers'; export { generateFormDefinition } from './generate-form-definition'; -export { mapFormToComponent } from './form-to-component'; diff --git a/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts b/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts new file mode 100644 index 000000000..40d0bad0d --- /dev/null +++ b/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts @@ -0,0 +1,85 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + StudioView, + DataStoreModelField, + TableDefinition, + DEFAULT_TABLE_CONFIG, + DEFAULT_TABLE_DEFINITION, + DEFAULT_TABLE_STYLE, + DEFAULT_TABLE_SOURCE, + GenericDataSchema, +} from '../types'; +import { orderAndFilterVisibleColumns } from './helpers'; + +/** + * Helper that turns the View model into definition that can be used to render + * Tables in the customer project and in Studio preview. + * @param table View, converted from the API shape. + * @param fields (Optional) holds type information about the DataStore model fields being represented. + * @returns a definition that translates to rendered JSX elements. + */ +export function generateTableDefinition(table: StudioView, dataSchema?: GenericDataSchema): TableDefinition { + const definition = DEFAULT_TABLE_DEFINITION; + + definition.tableStyle = { + ...DEFAULT_TABLE_STYLE, + ...table.style, + }; + + const { columns, ...rest } = table.viewConfiguration.table ?? {}; + + if (rest) { + definition.tableConfig = { + ...DEFAULT_TABLE_CONFIG, + ...rest, + }; + } + + definition.tableDataSource = { + ...DEFAULT_TABLE_SOURCE, + ...table.dataSource, + }; + + let fields: DataStoreModelField[] = []; + + if (table.dataSource.model) { + if (table.dataSource.type === 'DataStore') { + const dataModel = dataSchema?.models[table.dataSource.model]?.fields ?? {}; + fields = Object.entries(dataModel).map(([key, value]) => ({ + name: key, + type: value.dataType, + isReadOnly: value.readOnly, + isArray: value.isArray, + isRequired: value.required, + })); + } else { + const customModel = JSON.parse(table.dataSource.model); + fields = Object.keys(customModel).map((key) => ({ + name: key, + type: 'String', + isReadOnly: false, + isArray: false, + isRequired: false, + })); + } + } + + definition.columns = orderAndFilterVisibleColumns(columns ?? {}, fields); + + return definition; +} diff --git a/packages/codegen-ui/lib/generate-view-definition/helpers/index.ts b/packages/codegen-ui/lib/generate-view-definition/helpers/index.ts new file mode 100644 index 000000000..b8ffcb3d5 --- /dev/null +++ b/packages/codegen-ui/lib/generate-view-definition/helpers/index.ts @@ -0,0 +1,16 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export * from './order-filter-columns'; diff --git a/packages/codegen-ui/lib/generate-view-definition/helpers/order-filter-columns.ts b/packages/codegen-ui/lib/generate-view-definition/helpers/order-filter-columns.ts new file mode 100644 index 000000000..f7c904993 --- /dev/null +++ b/packages/codegen-ui/lib/generate-view-definition/helpers/order-filter-columns.ts @@ -0,0 +1,130 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { ColumnInfo, ColumnsMap, DataStoreModelField } from '../../types'; + +type ColumnNode = { + value: ColumnInfo; + next: ColumnNode | null; +}; + +type NodeMap = { [id: string]: ColumnNode }; + +const generateNodeMap = (columns: ColumnsMap, fields: DataStoreModelField[]): NodeMap => { + const map: NodeMap = {}; + + fields.forEach((field) => { + map[field.name] = { value: { header: field.name }, next: null }; + }); + + Object.entries(columns).forEach(([id, config]) => { + const current = { + value: { + ...map[id].value, + ...config, + }, + next: null, + }; + + map[id] = current; + }); + return map; +}; + +const applyDefaultOrdering = (fields: DataStoreModelField[], map: NodeMap) => { + const nodeMap = map; + for (let i = 1; i < fields.length; i += 1) { + const prev = fields[i - 1].name; + const next = fields[i].name; + nodeMap[prev].next = nodeMap[next]; + } +}; + +const applyOrderOverride = (map: NodeMap & { headOfMap$: ColumnNode }, defaultFirst: ColumnNode) => { + let newFirst = defaultFirst; + + const nodeMap = map; + + Object.values(nodeMap).forEach((node) => { + const current = node; + if (current.next?.value.position?.fixed) { + // extract first + newFirst = current.next; + current.next = current.next.next; + + // push first to front of linked list + newFirst.next = nodeMap.headOfMap$.next; + nodeMap.headOfMap$.next = newFirst; + } + }); + + Object.values(nodeMap).forEach((node) => { + const current = node; + const prev = current.next?.value.position?.rightOf; + if (prev && current.next) { + const toMove = current.next; + current.next = toMove.next; + + toMove.next = nodeMap[prev].next; + nodeMap[prev].next = toMove; + } + }); +}; + +const traverseAndCollectVisible = (first: ColumnNode): ColumnInfo[] => { + const ordered: ColumnInfo[] = []; + + let current: ColumnNode | null = first; + + while (current) { + if (!current.value.excluded) { + ordered.push(current.value); + } + current = current.next; + } + + return ordered; +}; + +export const orderAndFilterVisibleColumns = (columns: ColumnsMap, fields?: DataStoreModelField[]): ColumnInfo[] => { + let ordered: ColumnInfo[] = []; + + if (fields && fields.length > 0) { + const map = generateNodeMap(columns, fields); + + applyDefaultOrdering(fields, map); + + const defaultFirst = map[fields[0].name]; + + const mapWithHead: NodeMap & { headOfMap$: ColumnNode } = { + headOfMap$: { + value: { header: '' }, + next: defaultFirst, + }, + ...map, + }; + + applyOrderOverride(mapWithHead, defaultFirst); + + ordered = traverseAndCollectVisible(mapWithHead.headOfMap$.next!); + } + + // Spec for custom table generated from JSON is not defined yet. + // Thus, said table shape is currently unhandled + // TODO: Handle JSON-generated table after the spec is defined + + return ordered; +}; diff --git a/packages/codegen-ui/lib/generate-view-definition/index.ts b/packages/codegen-ui/lib/generate-view-definition/index.ts new file mode 100644 index 000000000..5f8f08fab --- /dev/null +++ b/packages/codegen-ui/lib/generate-view-definition/index.ts @@ -0,0 +1,16 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export { generateTableDefinition } from './generate-table-definition'; diff --git a/packages/codegen-ui/lib/generic-from-datastore.ts b/packages/codegen-ui/lib/generic-from-datastore.ts index 9144811cb..2e161c008 100644 --- a/packages/codegen-ui/lib/generic-from-datastore.ts +++ b/packages/codegen-ui/lib/generic-from-datastore.ts @@ -75,11 +75,12 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener let modelRelationship: GenericDataRelationshipType | undefined; if (relationshipType === 'HAS_MANY' && 'associatedWith' in field.association) { - const associatedModel = dataStoreSchema.models[field.type.model]; + const associatedModel = dataStoreSchema.models[relatedModelName]; const associatedFieldName = field.association.associatedWith; - const associatedField = associatedModel.fields[associatedFieldName]; + const associatedField = associatedModel?.fields[associatedFieldName]; // if the associated model is a join table, update relatedModelName to the actual related model if ( + associatedField && typeof associatedField.type === 'object' && 'model' in associatedField.type && associatedField.type.model === model.name @@ -95,7 +96,7 @@ export function getGenericFromDataStore(dataStoreSchema: DataStoreSchema): Gener } // if the associated model is not a join table, note implicit relationship for associated field } else { - addRelationship(fieldsWithImplicitRelationships, associatedModel.name, associatedFieldName, { + addRelationship(fieldsWithImplicitRelationships, relatedModelName, associatedFieldName, { type: 'HAS_ONE', relatedModelName: model.name, }); diff --git a/packages/codegen-ui/lib/renderer-helper.ts b/packages/codegen-ui/lib/renderer-helper.ts index 3148cfa3e..8b8794a15 100644 --- a/packages/codegen-ui/lib/renderer-helper.ts +++ b/packages/codegen-ui/lib/renderer-helper.ts @@ -25,6 +25,7 @@ import { StudioComponentProperty, StudioComponentSlotBinding, } from './types'; +import { breakpointSizes, BreakpointSizeType } from './utils/breakpoint-utils'; export function isStudioComponentWithBinding( component: StudioComponent | StudioComponentChild, @@ -52,6 +53,17 @@ export function isStudioComponentWithVariants( return 'variants' in component && component.variants !== undefined && component.variants.length > 0; } +export function isStudioComponentWithBreakpoints( + component: StudioComponent | StudioComponentChild, +): component is StudioComponent & Required> { + if (isStudioComponentWithVariants(component)) { + return component.variants.some((variant) => + breakpointSizes.includes(variant?.variantValues?.breakpoint as BreakpointSizeType), + ); + } + return false; +} + export function isDataPropertyBinding( prop: StudioComponentPropertyBinding, ): prop is StudioComponentDataPropertyBinding { diff --git a/packages/codegen-ui/lib/types/data.ts b/packages/codegen-ui/lib/types/data.ts index 203e0c705..ca088a1da 100644 --- a/packages/codegen-ui/lib/types/data.ts +++ b/packages/codegen-ui/lib/types/data.ts @@ -16,7 +16,14 @@ // exporting types and scalar functions from aws-amplify // as these will be used when loading in dataschema for form generation -export type { SchemaModel } from '@aws-amplify/datastore'; +export type { SchemaModel, ModelFields, ModelField, SchemaNonModels, Schema } from '@aws-amplify/datastore'; +export { isGraphQLScalarType } from '@aws-amplify/datastore'; + +export type SchemaEnums = Record; +export type SchemaEnum = { + name: string; + values: string[]; +}; type FieldType = string | { model: string } | { nonModel: string } | { enum: string }; diff --git a/packages/codegen-ui/lib/types/form/fields.ts b/packages/codegen-ui/lib/types/form/fields.ts index e654c6100..e2f991ee1 100644 --- a/packages/codegen-ui/lib/types/form/fields.ts +++ b/packages/codegen-ui/lib/types/form/fields.ts @@ -16,6 +16,7 @@ import { StudioFieldPosition } from './position'; import { StudioFieldInputConfig } from './input-config'; import { FieldValidationConfiguration } from './form-validation'; +import { DataFieldDataType } from '../data'; /** * Field configurations for StudioForm @@ -39,6 +40,7 @@ export type StudioGenericFieldConfig = { * The configuration for what type of input is used. */ inputType?: StudioFieldInputConfig; + dataType?: DataFieldDataType; } & StudioFieldConfig; export type StudioFormFieldConfig = StudioGenericFieldConfig | ExcludedStudioFieldConfig; diff --git a/packages/codegen-ui/lib/types/form/form-cta.ts b/packages/codegen-ui/lib/types/form/form-cta.ts new file mode 100644 index 000000000..7df27fc2d --- /dev/null +++ b/packages/codegen-ui/lib/types/form/form-cta.ts @@ -0,0 +1,38 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { StudioFieldPosition } from './position'; + +export type StudioFormCTAButton = + | { + excluded: true; + } + | { children?: string; position?: StudioFieldPosition }; + +/** + * Configuration for each of the specified CTA's + */ +export type StudioFormCTA = { + /** + * The position of the CTA's in the form when rendered + */ + position?: string; + + clear?: StudioFormCTAButton; + + cancel?: StudioFormCTAButton; + + submit?: StudioFormCTAButton; +}; diff --git a/packages/codegen-ui/lib/types/form/form-definition-element.ts b/packages/codegen-ui/lib/types/form/form-definition-element.ts index 9526964ab..f856ff95d 100644 --- a/packages/codegen-ui/lib/types/form/form-definition-element.ts +++ b/packages/codegen-ui/lib/types/form/form-definition-element.ts @@ -13,6 +13,16 @@ See the License for the specific language governing permissions and limitations under the License. */ + +import { DataFieldDataType } from '../data'; +import { FieldValidationConfiguration } from './form-validation'; +import { StudioFormValueMappings } from './input-config'; + +type FormDefinitionInputElementCommon = { + dataType?: DataFieldDataType; + validations?: (FieldValidationConfiguration & { immutable?: true })[]; +}; + export type FormDefinitionTextFieldElement = { componentType: 'TextField'; props: { @@ -22,12 +32,22 @@ export type FormDefinitionTextFieldElement = { isReadOnly?: boolean; placeholder?: string; defaultValue?: string; + type?: string; }; + studioFormComponentType: + | 'TextField' + | 'NumberField' + | 'DateField' + | 'TimeField' + | 'DateTimeField' + | 'IPAddressField' + | 'URLField' + | 'EmailField'; }; export type FormDefinitionSwitchFieldElement = { componentType: 'SwitchField'; - props: { label: string; defaultChecked?: boolean; isRequired?: boolean; isReadOnly?: boolean }; + props: { label: string; defaultChecked?: boolean; isDisabled?: boolean }; }; export type FormDefinitionPhoneNumberFieldElement = { @@ -47,7 +67,7 @@ export type FormDefinitionSelectFieldElement = { componentType: 'SelectField'; props: { label: string; descriptiveText?: string; placeholder?: string; isDisabled?: boolean }; // needs to be mapped as children of 'option' JSX elements - options: { value: string; children: string }[]; + valueMappings: StudioFormValueMappings; // 'selected' attr needs to be mapped onto the 'option' itself, not the SelectField defaultValue?: string; }; @@ -62,6 +82,7 @@ export type FormDefinitionTextAreaFieldElement = { placeholder?: string; defaultValue?: string; }; + studioFormComponentType: 'JSONField' | 'TextAreaField'; }; export type FormDefinitionSliderFieldElement = { @@ -117,8 +138,8 @@ export type FormDefinitionRadioGroupFieldElement = { descriptiveText?: string; isRequired?: boolean; }; - // needs to be mapped as children of 'Radio' components - radios: { value: string; children: string }[]; + // needs to be mapped as children of 'Radio' JSX elements + valueMappings: StudioFormValueMappings; }; export type FormDefinitionPasswordFieldElement = { @@ -133,7 +154,17 @@ export type FormDefinitionPasswordFieldElement = { }; }; -export type FormDefinitionInputElement = +export type FormDefinitionButtonElement = { + name: string; + componentType: 'Button'; + props: { + variation?: string; + children: string; + type?: string; + }; +}; + +export type FormDefinitionInputElement = ( | FormDefinitionTextFieldElement | FormDefinitionSwitchFieldElement | FormDefinitionPhoneNumberFieldElement @@ -144,7 +175,9 @@ export type FormDefinitionInputElement = | FormDefinitionToggleButtonElement | FormDefinitionCheckboxFieldElement | FormDefinitionRadioGroupFieldElement - | FormDefinitionPasswordFieldElement; + | FormDefinitionPasswordFieldElement +) & + FormDefinitionInputElementCommon; export type FormDefinitionHeadingElement = { componentType: 'Heading'; @@ -166,4 +199,7 @@ export type FormDefinitionSectionalElement = | FormDefinitionTextElement | FormDefinitionDividerElement; -export type FormDefinitionElement = FormDefinitionInputElement | FormDefinitionSectionalElement; +export type FormDefinitionElement = + | FormDefinitionInputElement + | FormDefinitionSectionalElement + | FormDefinitionButtonElement; diff --git a/packages/codegen-ui/lib/types/form/form-definition.ts b/packages/codegen-ui/lib/types/form/form-definition.ts index 0254379c7..501bc56ab 100644 --- a/packages/codegen-ui/lib/types/form/form-definition.ts +++ b/packages/codegen-ui/lib/types/form/form-definition.ts @@ -13,17 +13,47 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StudioFormStyle } from './style'; -import { FormDefinitionElement } from './form-definition-element'; +import { FormStyleConfig } from './style'; +import { FormDefinitionElement, FormDefinitionButtonElement } from './form-definition-element'; import { StudioGenericFieldConfig } from './fields'; export type ModelFieldsConfigs = { [key: string]: StudioGenericFieldConfig }; +export type ButtonConfig = { + buttonConfigs: { + submit?: FormDefinitionButtonElement; + cancel?: FormDefinitionButtonElement; + clear?: FormDefinitionButtonElement; + }; + position: string; + buttonMatrix: string[][]; +}; + export type FormDefinition = { form: { - layoutStyle: StudioFormStyle; + layoutStyle: { horizontalGap: FormStyleConfig; verticalGap: FormStyleConfig; outerPadding: FormStyleConfig }; }; elements: { [element: string]: FormDefinitionElement }; - buttons: { [key: string]: string }; + buttons: ButtonConfig; elementMatrix: string[][]; + inputFields?: string[]; }; + +export type FieldTypeMapKeys = + | 'ID' + | 'String' + | 'Int' + | 'Float' + | 'AWSDate' + | 'AWSTime' + | 'AWSDateTime' + | 'AWSTimestamp' + | 'AWSEmail' + | 'AWSURL' + | 'AWSIPAddress' + | 'Boolean' + | 'AWSJSON' + | 'AWSPhone' + | 'Enum' + | 'Relationship' + | 'NonModel'; diff --git a/packages/codegen-ui/lib/types/form/form-metadata.ts b/packages/codegen-ui/lib/types/form/form-metadata.ts new file mode 100644 index 000000000..54b076f61 --- /dev/null +++ b/packages/codegen-ui/lib/types/form/form-metadata.ts @@ -0,0 +1,41 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { DataFieldDataType } from '../data'; +import { FieldValidationConfiguration } from './form-validation'; +import { FormStyleConfig, StudioFormStyle } from './style'; + +/** + * Form Action type definition + */ +export type StudioFormActionType = 'create' | 'update'; + +export type FieldConfigMetadata = { + // ex. name field has a string validation type where the rule is char length > 5 + validationRules: FieldValidationConfiguration[]; + // component field is of type AWSTimestamp will need to map this to date then get time from date + dataType?: DataFieldDataType; + + isArray?: boolean; + componentType: string; +}; + +export type FormMetadata = { + id?: string; + formActionType: StudioFormActionType; + name: string; + fieldConfigs: Record; + layoutConfigs: Record; +}; diff --git a/packages/codegen-ui/lib/types/form/form-validation.ts b/packages/codegen-ui/lib/types/form/form-validation.ts index 0c412d4a3..9dc42669c 100644 --- a/packages/codegen-ui/lib/types/form/form-validation.ts +++ b/packages/codegen-ui/lib/types/form/form-validation.ts @@ -30,30 +30,57 @@ export enum ValidationTypes { JSON = 'JSON', IP_ADDRESS = 'IpAddress', URL = 'URL', + PHONE = 'Phone', } +export const ValidationTypeMapping: Record<'StringType' | 'NumberType', ValidationTypes[]> = { + StringType: [ + ValidationTypes.CONTAINS, + ValidationTypes.NOT_CONTAINS, + ValidationTypes.END_WITH, + ValidationTypes.START_WITH, + ValidationTypes.BE_BEFORE, + ValidationTypes.BE_AFTER, + ], + NumberType: [ + ValidationTypes.LESS_THAN_CHAR_LENGTH, + ValidationTypes.GREATER_THAN_CHAR_LENGTH, + ValidationTypes.LESS_THAN_NUM, + ValidationTypes.GREATER_THAN_NUM, + ValidationTypes.EQUAL_TO_NUM, + ], +}; + +export const IsStringTypeValidator = (validator: ValidationTypes): boolean => { + return ValidationTypeMapping.StringType.includes(validator); +}; + +export const IsNumberTypeValidator = (validator: ValidationTypes): boolean => { + return ValidationTypeMapping.NumberType.includes(validator); +}; + export type BaseValidation = { validationMessage?: string; }; export type StringValidationType = { type: ValidationTypes.CONTAINS | ValidationTypes.NOT_CONTAINS | ValidationTypes.END_WITH | ValidationTypes.START_WITH; - values: string[]; + strValues: string[]; +} & BaseValidation; + +export type DateValidationType = { + type: ValidationTypes.BE_BEFORE | ValidationTypes.BE_AFTER; + strValues: string[]; } & BaseValidation; export type StringLengthValidationType = { type: ValidationTypes.LESS_THAN_CHAR_LENGTH | ValidationTypes.GREATER_THAN_CHAR_LENGTH; - values: number; + numValues: number[]; } & BaseValidation; export type NumberValidationType = { type: ValidationTypes.LESS_THAN_NUM | ValidationTypes.GREATER_THAN_NUM | ValidationTypes.EQUAL_TO_NUM; - values: number[] | number; -} & BaseValidation; - -export type DateValidationType = { - type: ValidationTypes.BE_BEFORE | ValidationTypes.BE_AFTER; - values: string | number; + numValues: number[]; } & BaseValidation; export type GenericValidationType = { @@ -62,14 +89,15 @@ export type GenericValidationType = { | ValidationTypes.EMAIL | ValidationTypes.JSON | ValidationTypes.IP_ADDRESS - | ValidationTypes.URL; + | ValidationTypes.URL + | ValidationTypes.PHONE; } & BaseValidation; export type FieldValidationConfiguration = | StringValidationType + | DateValidationType | StringLengthValidationType | NumberValidationType - | DateValidationType | GenericValidationType; export type ValidationResponse = { hasError: boolean; errorMessage?: string }; diff --git a/packages/codegen-ui/lib/types/form/index.ts b/packages/codegen-ui/lib/types/form/index.ts index 59911686c..542aee7fe 100644 --- a/packages/codegen-ui/lib/types/form/index.ts +++ b/packages/codegen-ui/lib/types/form/index.ts @@ -17,27 +17,29 @@ import { StudioFormStyle } from './style'; import { StudioFormFields, StudioFormFieldConfig, StudioGenericFieldConfig } from './fields'; import { SectionalElement } from './sectional-element'; -import { FormDefinition, ModelFieldsConfigs } from './form-definition'; -import { StudioFieldInputConfig } from './input-config'; +import { FormDefinition, ModelFieldsConfigs, FieldTypeMapKeys, ButtonConfig } from './form-definition'; +import { StudioFieldInputConfig, StudioFormValueMappings } from './input-config'; import { StudioFieldPosition } from './position'; +import { StudioFormCTA } from './form-cta'; +import { FormMetadata, FieldConfigMetadata, StudioFormActionType } from './form-metadata'; + +export type StudioDataSourceType = 'DataStore' | 'Custom'; /** * Data type definition for StudioForm */ -type StudioFormDataType = { - dataSourceType: 'DataStore' | 'Custom'; +export type StudioFormDataType = { + dataSourceType: StudioDataSourceType; dataTypeName: string; }; -/** - * Form Action type definition - */ -type StudioFormActionType = 'create' | 'update'; /** * This is the base type for all StudioForms */ export type StudioForm = { + id?: string; + name: string; formActionType: StudioFormActionType; @@ -49,18 +51,50 @@ export type StudioForm = { sectionalElements: { [elementName: string]: SectionalElement }; style: StudioFormStyle; + + cta: StudioFormCTA; }; +export type FormInputType = + | 'TextField' + | 'TextAreaField' + | 'PasswordField' + | 'SliderField' + | 'StepperField' + | 'SwitchField' + | 'ToggleButton' + | 'CheckboxField' + | 'RadioGroupField' + | 'PhoneNumberField' + | 'SelectField' + | 'NumberField' + | 'DateField' + | 'TimeField' + | 'DateTimeField' + | 'IPAddressField' + | 'URLField' + | 'EmailField' + | 'JSONField' + | 'ArrayField'; + export * from './form-definition-element'; +export * from './style'; +export * from './form-validation'; +export * from './form-cta'; export type { - StudioFormStyle, SectionalElement, StudioFormFieldConfig, + StudioFormActionType, FormDefinition, + FormMetadata, + FieldConfigMetadata, StudioFieldInputConfig, StudioGenericFieldConfig, StudioFormFields, ModelFieldsConfigs, StudioFieldPosition, + FieldTypeMapKeys, + StudioFormValueMappings, + ButtonConfig, }; diff --git a/packages/codegen-ui/lib/types/form/input-config.ts b/packages/codegen-ui/lib/types/form/input-config.ts index 7236817a3..9569d9cc2 100644 --- a/packages/codegen-ui/lib/types/form/input-config.ts +++ b/packages/codegen-ui/lib/types/form/input-config.ts @@ -14,6 +14,26 @@ limitations under the License. */ +import { + FixedStudioComponentProperty, + ConcatenatedStudioComponentProperty, + ConditionalStudioComponentProperty, + BoundStudioComponentProperty, +} from '../properties'; +import { StudioComponentPropertyBinding } from '../bindings'; + +export type StudioFormInputFieldProperty = + | FixedStudioComponentProperty + | ConcatenatedStudioComponentProperty + | ConditionalStudioComponentProperty + | BoundStudioComponentProperty; + +export type StudioFormValueMappings = { + values: { displayValue?: StudioFormInputFieldProperty; value: StudioFormInputFieldProperty }[]; + + bindingProperties?: { [propertyName: string]: StudioComponentPropertyBinding }; +}; + // represents API shape after type casting export type StudioFieldInputConfig = { type: string; @@ -32,7 +52,7 @@ export type StudioFieldInputConfig = { defaultCountryCode?: string; - valueMappings?: { value: string; displayValue: string }[]; + valueMappings?: StudioFormValueMappings; name?: string; @@ -43,4 +63,6 @@ export type StudioFieldInputConfig = { step?: number; value?: string; + + isArray?: boolean; }; diff --git a/packages/codegen-ui/lib/types/form/sectional-element.ts b/packages/codegen-ui/lib/types/form/sectional-element.ts index f003f7b7e..84f866653 100644 --- a/packages/codegen-ui/lib/types/form/sectional-element.ts +++ b/packages/codegen-ui/lib/types/form/sectional-element.ts @@ -20,7 +20,7 @@ import { StudioFieldPosition } from './position'; */ export type SectionalElement = { - position: StudioFieldPosition; + position?: StudioFieldPosition; type: string; level?: number; diff --git a/packages/codegen-ui/lib/types/form/style.ts b/packages/codegen-ui/lib/types/form/style.ts index 3414bee29..97a17166a 100644 --- a/packages/codegen-ui/lib/types/form/style.ts +++ b/packages/codegen-ui/lib/types/form/style.ts @@ -13,21 +13,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -type FormStyleConfigCommon = { +export type FormStyleConfigCommon = { tokenReference?: string; }; -type FormStyleConfig = { +export type FormStyleConfig = { value?: string; } & FormStyleConfigCommon; -type FormAlignmentConfig = { - value?: 'left' | 'center' | 'right'; -} & FormStyleConfigCommon; - export type StudioFormStyle = { horizontalGap?: FormStyleConfig; verticalGap?: FormStyleConfig; outerPadding?: FormStyleConfig; - alignment?: FormAlignmentConfig; }; diff --git a/packages/codegen-ui/lib/types/index.ts b/packages/codegen-ui/lib/types/index.ts index e60bbd429..f152ef4f2 100644 --- a/packages/codegen-ui/lib/types/index.ts +++ b/packages/codegen-ui/lib/types/index.ts @@ -13,6 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { StudioComponent } from './components'; +import { StudioForm } from './form'; +import { StudioTheme } from './theme'; +import { StudioView } from './view'; + export * from './actions'; export * from './components'; export * from './bindings'; @@ -26,3 +31,5 @@ export * from './form'; export * from './view'; export * from './data'; export * from './string-format'; + +export type StudioSchema = StudioComponent | StudioForm | StudioView | StudioTheme; diff --git a/packages/codegen-ui/lib/types/string-format.ts b/packages/codegen-ui/lib/types/string-format.ts index b77d2d0f1..604999582 100644 --- a/packages/codegen-ui/lib/types/string-format.ts +++ b/packages/codegen-ui/lib/types/string-format.ts @@ -30,20 +30,26 @@ export enum TIME { } export type DateFormat = { + type?: 'DateFormat'; dateFormat: 'locale' | 'MM/DD/YYYY' | 'DD.MM.YYYY' | 'YYYY.MM.DD' | 'Mmm DD, YYYY'; }; export type TimeFormat = { + type?: 'TimeFormat'; timeFormat: 'locale' | 'hours12' | 'hours24'; }; -export type DateTimeFormat = { - dateTimeFormat: - | 'locale' - | { - dateFormat: DateFormat['dateFormat']; - timeFormat: TimeFormat['timeFormat']; - }; +export type NonLocaleDateTimeFormat = { + type?: 'NonLocaleDateTimeFormat'; + nonLocaleDateTimeFormat: { + dateFormat: DateFormat['dateFormat']; + timeFormat: TimeFormat['timeFormat']; + }; }; -export type StringFormat = DateFormat | TimeFormat | DateTimeFormat; +export type LocaleDateTimeFormat = { + type?: 'LocaleDateTimeFormat'; + localeDateTimeFormat: 'locale'; +}; + +export type StringFormat = DateFormat | TimeFormat | NonLocaleDateTimeFormat | LocaleDateTimeFormat; diff --git a/packages/codegen-ui/lib/types/view/defaults.ts b/packages/codegen-ui/lib/types/view/defaults.ts new file mode 100644 index 000000000..93d81e23c --- /dev/null +++ b/packages/codegen-ui/lib/types/view/defaults.ts @@ -0,0 +1,65 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { ViewStyle } from './style'; +import { ColumnConfig } from './table'; +import { TableDefinition, OverallTableConfig } from './table-definition'; +import { ViewDataTypeConfig } from './view'; + +export const DEFAULT_COLUMN_CONFIG: ColumnConfig = { + excluded: false, + isSticky: false, + label: '', + maxDisplayItems: 20, + position: {}, + sortable: false, + valueFormatting: undefined, +}; + +export const DEFAULT_TABLE_CONFIG: OverallTableConfig = { + type: 'Table', + table: { + disableHeaders: false, + highlightOnHover: false, + enableOnRowClick: false, + }, +}; + +export const DEFAULT_TABLE_STYLE: ViewStyle = { + alignment: { + value: 'left', + }, + horizontalGap: { + value: '0', + }, + verticalGap: { + value: '0', + }, + outerPadding: { + value: '10px', + }, +}; + +export const DEFAULT_TABLE_SOURCE: ViewDataTypeConfig = { + type: 'DataStore', +}; + +export const DEFAULT_TABLE_DEFINITION: TableDefinition = { + tableStyle: DEFAULT_TABLE_STYLE, + tableConfig: DEFAULT_TABLE_CONFIG, + tableDataSource: DEFAULT_TABLE_SOURCE, + columns: [], +}; diff --git a/packages/codegen-ui/lib/types/view/index.ts b/packages/codegen-ui/lib/types/view/index.ts index 0fa6ab5d2..6cb033618 100644 --- a/packages/codegen-ui/lib/types/view/index.ts +++ b/packages/codegen-ui/lib/types/view/index.ts @@ -19,3 +19,6 @@ export * from './style'; export * from './table'; export * from './value'; export * from './view'; +export * from './table-definition'; +export * from './defaults'; +export * from './view-metadata'; diff --git a/packages/codegen-ui/lib/types/view/style.ts b/packages/codegen-ui/lib/types/view/style.ts index 8f33391c8..a5d91533e 100644 --- a/packages/codegen-ui/lib/types/view/style.ts +++ b/packages/codegen-ui/lib/types/view/style.ts @@ -17,7 +17,7 @@ export declare type ViewAlignment = 'left' | 'right' | 'center'; export interface ViewAlignmentConfig { - tokenReference?: String; + tokenReference?: string; value?: ViewAlignment; } @@ -29,6 +29,6 @@ export interface ViewStyle { } export interface ViewStyleConfig { - tokenReference?: String; - value?: String; + tokenReference?: string; + value?: string; } diff --git a/packages/codegen-ui/lib/types/view/table-definition.ts b/packages/codegen-ui/lib/types/view/table-definition.ts new file mode 100644 index 000000000..7b75e26d8 --- /dev/null +++ b/packages/codegen-ui/lib/types/view/table-definition.ts @@ -0,0 +1,30 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { ViewStyle } from './style'; +import { ColumnConfig } from './table'; +import { TableConfiguration, ViewDataTypeConfig } from './view'; + +export type OverallTableConfig = Omit; + +export type ColumnInfo = ColumnConfig & { header: string }; + +export type TableDefinition = { + tableStyle: ViewStyle; + tableConfig: OverallTableConfig; + tableDataSource: ViewDataTypeConfig; + columns: ColumnInfo[]; +}; diff --git a/packages/codegen-ui/lib/types/view/table.ts b/packages/codegen-ui/lib/types/view/table.ts index e1dd16c8a..15262c8be 100644 --- a/packages/codegen-ui/lib/types/view/table.ts +++ b/packages/codegen-ui/lib/types/view/table.ts @@ -23,7 +23,7 @@ export interface ColumnConfig { label?: string; maxDisplayItems?: number; position?: FieldPosition; - sortable?: Boolean; + sortable?: boolean; valueFormatting?: ViewValueFormatting; } export interface ColumnsMap { diff --git a/packages/codegen-ui/lib/types/view/view-metadata.ts b/packages/codegen-ui/lib/types/view/view-metadata.ts new file mode 100644 index 000000000..e2ff96d2f --- /dev/null +++ b/packages/codegen-ui/lib/types/view/view-metadata.ts @@ -0,0 +1,24 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { ViewValueFormatting } from './value'; + +export type ViewMetadata = { + id?: string; + name: string; + // Stores the configured formatting for each field (table column) + fieldFormatting: { [fieldName: string]: ViewValueFormatting }; +}; diff --git a/packages/codegen-ui/lib/types/view/view.ts b/packages/codegen-ui/lib/types/view/view.ts index 26c3e072a..b0027ee01 100644 --- a/packages/codegen-ui/lib/types/view/view.ts +++ b/packages/codegen-ui/lib/types/view/view.ts @@ -17,40 +17,50 @@ import { StudioComponentPredicate, StudioComponentSort } from '../bindings'; import { ViewStyle } from './style'; import { ColumnsMap } from './table'; -export interface View { - appId: String; +export interface StudioView { + appId?: string; dataSource: ViewDataTypeConfig; - environmentName: String; + environmentName?: string; id: string; name: ViewName; - schemaVersion: String; - sourceId?: String; + schemaVersion: string; + sourceId?: string; style: ViewStyle; viewConfiguration: ViewConfiguration; } -export interface ViewConfiguration { - columns?: ColumnsMap; - disableHeaders?: Boolean; - highlightOnHover?: Boolean; +export interface BaseViewConfiguration { type: ViewType; } +export interface TableConfiguration extends BaseViewConfiguration { + type: 'Table'; + table: { + columns?: ColumnsMap; + disableHeaders?: boolean; + highlightOnHover?: boolean; + enableOnRowClick?: boolean; + }; +} + +// Append other configuration types here +export type ViewConfiguration = TableConfiguration; + export interface ViewDataTypeConfig { identifiers?: string[]; - model?: String; + model?: string; predicate?: StudioComponentPredicate; sort?: StudioComponentSort[]; type: 'DataStore' | 'Custom'; } -export declare type ViewList = View[]; +export declare type ViewList = StudioView[]; export declare type ViewName = string; export interface ViewSummary { - appId: String; - environmentName: String; + appId: string; + environmentName: string; id: string; name: ViewName; } diff --git a/packages/codegen-ui/lib/utils/breakpoint-utils.ts b/packages/codegen-ui/lib/utils/breakpoint-utils.ts new file mode 100644 index 000000000..643fe6bae --- /dev/null +++ b/packages/codegen-ui/lib/utils/breakpoint-utils.ts @@ -0,0 +1,49 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { StudioComponent } from '../types'; + +export const breakpointSizes = ['base', 'small', 'medium', 'large', 'xl', 'xxl'] as const; +export type BreakpointSizeType = typeof breakpointSizes[number]; +export const bpWeights: Record = { + base: 0, + small: 1, + medium: 2, + large: 3, + xl: 4, + xxl: 5, +}; + +/** + * sorts the breakpoints to the following order + * + * ['base', 'small', 'medium', 'large', 'xl', 'xxl'] + */ +export const sortBreakpoints = (bs: BreakpointSizeType[]): BreakpointSizeType[] => { + return bs.sort((lhs, rhs) => { + return bpWeights[lhs] - bpWeights[rhs]; + }); +}; + +export const getBreakpoints = (component: StudioComponent & Required>) => { + const breakpoints = component.variants.reduce((acc, variant) => { + if (variant.variantValues?.breakpoint) { + acc.push(variant.variantValues.breakpoint as BreakpointSizeType); + } + return acc; + }, []); + return sortBreakpoints(breakpoints); +}; diff --git a/packages/codegen-ui/lib/utils/component-metadata.ts b/packages/codegen-ui/lib/utils/component-metadata.ts index eab4324cb..e80511263 100644 --- a/packages/codegen-ui/lib/utils/component-metadata.ts +++ b/packages/codegen-ui/lib/utils/component-metadata.ts @@ -21,6 +21,7 @@ import { StudioComponentProperty, StudioComponentPropertyBinding, StateReference, + FormMetadata, } from '../types'; import { StateReferenceMetadata, computeStateReferenceMetadata } from './state-reference-metadata'; @@ -29,6 +30,7 @@ export type ComponentMetadata = { requiredDataModels: string[]; stateReferences: StateReferenceMetadata[]; componentNameToTypeMap: Record; + formMetadata?: FormMetadata; }; /** diff --git a/packages/codegen-ui/lib/utils/form-component-metadata.ts b/packages/codegen-ui/lib/utils/form-component-metadata.ts new file mode 100644 index 000000000..44c84ab2f --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-component-metadata.ts @@ -0,0 +1,79 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { + FormDefinition, + FormMetadata, + FieldConfigMetadata, + StudioForm, + FieldValidationConfiguration, + FormDefinitionElement, + FormDefinitionInputElement, + StudioFieldInputConfig, + GenericDataSchema, +} from '../types'; + +export const getFormFieldStateName = (formName: string) => { + return [formName.charAt(0).toLowerCase() + formName.slice(1), 'Fields'].join(''); +}; + +function elementIsInput(element: FormDefinitionElement): element is FormDefinitionInputElement { + return element.componentType !== 'Text' && element.componentType !== 'Divider' && element.componentType !== 'Heading'; +} + +export const mapFormMetadata = ( + form: StudioForm, + formDefinition: FormDefinition, + dataSchema?: GenericDataSchema | undefined, +): FormMetadata => { + const inputElementEntries = Object.entries(formDefinition.elements).filter(([, element]) => elementIsInput(element)); + return { + id: form.id, + name: form.name, + formActionType: form.formActionType, + layoutConfigs: { + ...formDefinition.form.layoutStyle, + }, + fieldConfigs: inputElementEntries.reduce>((configs, [name, config]) => { + const updatedConfigs = configs; + const metadata: FieldConfigMetadata = { + validationRules: [], + componentType: config.componentType, + }; + if ('validations' in config && config.validations) { + metadata.validationRules = config.validations.map((validation) => { + const updatedValidation = validation; + delete updatedValidation.immutable; + return updatedValidation; + }); + } + if ('dataType' in config && config.dataType) { + metadata.dataType = config.dataType; + } + if (form.dataType.dataSourceType === 'DataStore' && dataSchema) { + const modelFields = dataSchema.models[form.dataType.dataTypeName].fields; + metadata.isArray = modelFields[name]?.isArray; + } + if (form.fields[name] && 'inputType' in form.fields[name]) { + const { inputType } = form.fields[name] as { inputType: StudioFieldInputConfig }; + if (inputType.isArray) { + metadata.isArray = inputType.isArray; + } + } + updatedConfigs[name] = metadata; + return updatedConfigs; + }, {}), + }; +}; diff --git a/packages/codegen-ui/lib/utils/form-to-component/helpers/map-cta-buttons.ts b/packages/codegen-ui/lib/utils/form-to-component/helpers/map-cta-buttons.ts new file mode 100644 index 000000000..a745a190d --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-to-component/helpers/map-cta-buttons.ts @@ -0,0 +1,113 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { StudioComponentChild, FormDefinition, ButtonConfig } from '../../../types'; +import { FormDefinitionButtonElement } from '../../../types/form/form-definition-element'; + +const mapButtonPosition = (buttonElement: FormDefinitionButtonElement): StudioComponentChild => { + return { + componentType: buttonElement.componentType, + name: buttonElement.name, + properties: { + children: { + value: buttonElement.props.children, + }, + type: { + value: buttonElement.props.type ? buttonElement.props.type : 'button', + }, + ...(buttonElement.props.variation && { variation: { value: buttonElement.props.variation } }), + }, + }; +}; + +const mapButtonNameToConfig = (name: string, config: ButtonConfig) => { + if (name === 'clear') { + return config.buttonConfigs.clear; + } + if (name === 'cancel') { + return config.buttonConfigs.cancel; + } + return config.buttonConfigs.submit; +}; + +export const ctaButtonMapper = (formDefinition: FormDefinition): StudioComponentChild => { + const CTAComponent: StudioComponentChild = { + name: 'CTAFlex', + componentType: 'Flex', + properties: { + justifyContent: { + value: 'space-between', + }, + }, + children: [], + }; + + formDefinition.buttons.buttonMatrix[0].forEach((button) => { + if (Object.keys(formDefinition.buttons.buttonConfigs).includes(button)) { + const config = mapButtonNameToConfig(button, formDefinition.buttons); + if (config) { + const buttonDefinition = mapButtonPosition(config); + CTAComponent.children?.push(buttonDefinition); + } + } + }); + + const rightAlignCTA: StudioComponentChild = { + componentType: 'Flex', + name: 'SubmitAndResetFlex', + properties: {}, + children: [], + }; + + formDefinition.buttons.buttonMatrix[1].forEach((button) => { + if (Object.keys(formDefinition.buttons.buttonConfigs).includes(button)) { + const config = mapButtonNameToConfig(button, formDefinition.buttons); + if (config) { + const buttonDefinition = mapButtonPosition(config); + rightAlignCTA.children?.push(buttonDefinition); + } + } + }); + + CTAComponent.children?.push(rightAlignCTA); + + return CTAComponent; +}; + +export const addCTAPosition = ( + formChildren: StudioComponentChild[], + position: string, + buttons: StudioComponentChild, +) => { + const updatedFormChildren = formChildren || []; + + switch (position) { + case 'top': + updatedFormChildren.splice(0, 0, buttons); + break; + case 'bottom': + updatedFormChildren.splice(formChildren.length, 0, buttons); + break; + case 'top_and_bottom': + updatedFormChildren.splice(0, 0, buttons); + updatedFormChildren.splice(formChildren.length, 0, buttons); + break; + default: + updatedFormChildren.splice(formChildren.length, 0, buttons); + break; + } + + return updatedFormChildren; +}; diff --git a/packages/codegen-ui/lib/utils/form-to-component/helpers/map-element-children.ts b/packages/codegen-ui/lib/utils/form-to-component/helpers/map-element-children.ts new file mode 100644 index 000000000..a6b2cded5 --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-to-component/helpers/map-element-children.ts @@ -0,0 +1,60 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { + FormDefinitionElement, + FormDefinitionRadioGroupFieldElement, + FormDefinitionSelectFieldElement, + StudioComponentChild, +} from '../../../types'; + +type MapElementChildrenReturnValue = { children: StudioComponentChild[] }; + +function mapOptions( + elementName: string, + element: FormDefinitionSelectFieldElement | FormDefinitionRadioGroupFieldElement, +): { children: StudioComponentChild[] } { + const options = element.valueMappings.values.map(({ displayValue, value }, index) => { + const optionType = element.componentType === 'RadioGroupField' ? 'Radio' : 'option'; + + const option: StudioComponentChild = { + name: `${elementName}${optionType}${index}`, + componentType: optionType, + properties: { + children: displayValue ?? value, + value, + }, + }; + + if (element.componentType === 'SelectField' && 'value' in value && value.value === element.defaultValue) { + option.properties.selected = { value: true }; + } + + return option; + }); + + return { children: options }; +} + +export function mapElementChildren(elementName: string, element: FormDefinitionElement): MapElementChildrenReturnValue { + switch (element.componentType) { + case 'SelectField': + case 'RadioGroupField': + return mapOptions(elementName, element); + + default: + return { children: [] }; + } +} diff --git a/packages/codegen-ui/lib/utils/form-to-component/index.ts b/packages/codegen-ui/lib/utils/form-to-component/index.ts new file mode 100644 index 000000000..a1a3c1993 --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-to-component/index.ts @@ -0,0 +1,16 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +export { mapFormDefinitionToComponent } from './map-form-definition-to-component'; diff --git a/packages/codegen-ui/lib/utils/form-to-component/map-form-definition-to-component.ts b/packages/codegen-ui/lib/utils/form-to-component/map-form-definition-to-component.ts new file mode 100644 index 000000000..3db85b415 --- /dev/null +++ b/packages/codegen-ui/lib/utils/form-to-component/map-form-definition-to-component.ts @@ -0,0 +1,98 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { + StudioComponent, + StudioComponentChild, + FormDefinition, + FormDefinitionElement, + StudioComponentProperties, +} from '../../types'; +import { mapElementChildren } from './helpers/map-element-children'; +import { ctaButtonMapper, addCTAPosition } from './helpers/map-cta-buttons'; + +const mapFormElementProps = (element: FormDefinitionElement) => { + const props: StudioComponentProperties = {}; + Object.entries(element.props).forEach(([key, value]) => { + props[key] = { value: `${value}`, type: `${typeof value}` }; + }); + return props; +}; + +/** + * will wrap the studio component children in a row grid + */ +export const wrapInRowGrid = ( + idx: number, + rowLength: number, + children: StudioComponentChild[], +): StudioComponentChild[] => { + return [ + { + name: `RowGrid${idx}`, + componentType: 'Grid', + properties: { + columnGap: { value: 'inherit' }, + rowGap: { value: 'inherit' }, + templateColumns: { value: `repeat(${rowLength}, auto)` }, + }, + children, + }, + ]; +}; + +const getFormElementChildren = (formDefinition: FormDefinition): StudioComponentChild[] => + formDefinition.elementMatrix.reduce( + (acc: StudioComponentChild[], row: string[], rowIdx: number) => { + const children = row.map((column) => { + const element: FormDefinitionElement = formDefinition.elements[column]; + return { + name: column, + componentType: element.componentType, + properties: mapFormElementProps(element), + children: mapElementChildren(column, element).children, + }; + }); + // if we have more than one element in a row we create a rowGrid to display the columns for those children + acc.push(...(row.length > 1 ? wrapInRowGrid(rowIdx, row.length, children) : children)); + return acc; + }, + [], + ); + +export const mapFormDefinitionToComponent = (name: string, formDefinition: FormDefinition) => { + const ctaComponent = ctaButtonMapper(formDefinition); + + const formChildren = addCTAPosition( + getFormElementChildren(formDefinition), + formDefinition.buttons.position, + ctaComponent, + ); + + const component: StudioComponent = { + name, + componentType: 'Grid', + properties: { + as: { value: 'form' }, + }, + bindingProperties: { + onCancel: { type: 'Event' }, + }, + events: {}, + children: formChildren, + }; + + return component; +}; diff --git a/packages/codegen-ui/lib/utils/index.ts b/packages/codegen-ui/lib/utils/index.ts index 670fc1986..c3e37108e 100644 --- a/packages/codegen-ui/lib/utils/index.ts +++ b/packages/codegen-ui/lib/utils/index.ts @@ -17,3 +17,14 @@ export * from './component-metadata'; export * from './component-tree'; export * from './state-reference-metadata'; export * from './string-formatter'; +export * from './form-component-metadata'; +export * from './form-to-component'; +export * from './breakpoint-utils'; +export const ControlledComponents = ['StepperField', 'SliderField', 'SelectField']; +/** + * given the component returns true if the component is a controlled component + * + * @param componentType + * @returns + */ +export const isControlledComponent = (componentType: string): boolean => ControlledComponents.includes(componentType); diff --git a/packages/codegen-ui/lib/utils/string-formatter.ts b/packages/codegen-ui/lib/utils/string-formatter.ts index a644011dd..4f0b5c15c 100644 --- a/packages/codegen-ui/lib/utils/string-formatter.ts +++ b/packages/codegen-ui/lib/utils/string-formatter.ts @@ -13,12 +13,48 @@ See the License for the specific language governing permissions and limitations under the License. */ - -import { DateFormat, DateTimeFormat, TimeFormat } from '../types'; +import { DateFormat, NonLocaleDateTimeFormat, LocaleDateTimeFormat, TimeFormat } from '../types/string-format'; + +const monthToShortMon: { [mon: string]: string } = { + '1': 'Jan', + '2': 'Feb', + '3': 'Mar', + '4': 'Apr', + '5': 'May', + '6': 'Jun', + '7': 'Jul', + '8': 'Aug', + '9': 'Sep', + '10': 'Oct', + '11': 'Nov', + '12': 'Dec', +}; const invalidDateStr = 'Invalid Date'; -export function formatDate(date: string, format: DateFormat['dateFormat']): string { +type DateFormatInput = { + type: 'DateFormat'; + format: DateFormat; +}; + +type LocaleDateTimeFormatInput = { + type: 'LocaleDateTimeFormat'; + format: LocaleDateTimeFormat; +}; + +type NonLocaleDateTimeFormatInput = { + type: 'NonLocaleDateTimeFormat'; + format: NonLocaleDateTimeFormat; +}; + +type TimeFormatInput = { + type: 'TimeFormat'; + format: TimeFormat; +}; + +type FormatInput = DateFormatInput | TimeFormatInput | NonLocaleDateTimeFormatInput | LocaleDateTimeFormatInput; + +export function formatDate(date: string, dateFormat: DateFormat['dateFormat']): string { if (date === undefined || date === null) { return date; } @@ -33,9 +69,12 @@ export function formatDate(date: string, format: DateFormat['dateFormat']): stri const year = splitDate[0]; const month = splitDate[1]; - const day = validDate.toLocaleString('en-us', { day: '2-digit' }); + const day = splitDate[2]; - switch (format) { + // Remove leading zeroes + const truncatedMonth = month.replace(/^0+/, ''); + + switch (dateFormat) { case 'locale': return validDate.toLocaleDateString(); case 'YYYY.MM.DD': @@ -45,13 +84,13 @@ export function formatDate(date: string, format: DateFormat['dateFormat']): stri case 'MM/DD/YYYY': return `${month}/${day}/${year}`; case 'Mmm DD, YYYY': - return `${validDate.toLocaleString('en-us', { month: 'short' })} ${day}, ${year}`; + return `${monthToShortMon[truncatedMonth]} ${day}, ${year}`; default: return date; } } -export function formatTime(time: string, format: TimeFormat['timeFormat']): string { +export function formatTime(time: string, timeFormat: TimeFormat['timeFormat']): string { if (time === undefined || time === null) { return time; } @@ -74,7 +113,7 @@ export function formatTime(time: string, format: TimeFormat['timeFormat']): stri return time; } - switch (format) { + switch (timeFormat) { case 'locale': return validTime.toLocaleTimeString(); case 'hours24': @@ -86,7 +125,10 @@ export function formatTime(time: string, format: TimeFormat['timeFormat']): stri } } -export function formatDateTime(dateTimeStr: string, format: DateTimeFormat['dateTimeFormat']): string { +export function formatDateTime( + dateTimeStr: string, + dateTimeFormat: NonLocaleDateTimeFormat['nonLocaleDateTimeFormat'] | LocaleDateTimeFormat['localeDateTimeFormat'], +): string { if (dateTimeStr === undefined || dateTimeStr === null) { return dateTimeStr; } @@ -101,12 +143,27 @@ export function formatDateTime(dateTimeStr: string, format: DateTimeFormat['date return dateTimeStr; } - if (format === 'locale') { + if (dateTimeFormat === 'locale') { return dateTime.toLocaleString(); } const dateAndTime = dateTime.toISOString().split('T'); - const date = formatDate(dateAndTime[0], format.dateFormat); - const time = formatTime(dateAndTime[1], format.timeFormat); + const date = formatDate(dateAndTime[0], dateTimeFormat.dateFormat); + const time = formatTime(dateAndTime[1], dateTimeFormat.timeFormat); return `${date} - ${time}`; } + +export function formatter(value: string, formatterInput: FormatInput) { + switch (formatterInput.type) { + case 'DateFormat': + return formatDate(value, formatterInput.format.dateFormat); + case 'TimeFormat': + return formatTime(value, formatterInput.format.timeFormat); + case 'LocaleDateTimeFormat': + return formatDateTime(value, formatterInput.format.localeDateTimeFormat); + case 'NonLocaleDateTimeFormat': + return formatDateTime(value, formatterInput.format.nonLocaleDateTimeFormat); + default: + return value; + } +} diff --git a/packages/codegen-ui/lib/validation-helper.ts b/packages/codegen-ui/lib/validation-helper.ts index b6633b331..3b81f4d65 100644 --- a/packages/codegen-ui/lib/validation-helper.ts +++ b/packages/codegen-ui/lib/validation-helper.ts @@ -15,12 +15,18 @@ */ import * as yup from 'yup'; import { InvalidInputError } from './errors'; -import { StudioGenericEvent } from './types'; +import { StudioGenericEvent, StudioSchema } from './types'; const alphaNumString = () => { return yup.string().matches(/^[a-zA-Z0-9]*$/, { message: 'Expected an alphanumeric string' }); }; +const pascalCaseAlphaNumString = () => { + return yup + .string() + .matches(/^[A-Z][a-zA-Z0-9]*$/, { message: 'Expected an alphanumeric string with capital first letter' }); +}; + const alphaNumNoLeadingNumberString = () => { return yup .string() @@ -96,7 +102,7 @@ const studioComponentChildSchema: any = yup.object({ const studioComponentSchema = yup .object({ - name: alphaNumString().required(), + name: pascalCaseAlphaNumString().required(), id: yup.string().nullable(), sourceId: yup.string().nullable(), schemaVersion: yup.lazy(() => schemaVersionSchema().nullable()), @@ -177,10 +183,44 @@ const studioThemeSchema = yup.object({ overrides: yup.array(studioThemeValuesSchema).nullable(), }); +/** + * Form Schema Definitions + */ +const studioFormSchema = yup.object({ + name: pascalCaseAlphaNumString().required(), + id: yup.string().nullable(), + formActionType: yup.string().matches(new RegExp('(create|update)')), + dataType: yup.object({ + dataSourceType: yup.string().matches(new RegExp('(DataStore|Custom)')), + dataTypeName: yup.string().required(), + }), + fields: yup.object().nullable(), + sectionalElements: yup.object().nullable(), + style: yup.object().nullable(), + cta: yup.object().nullable(), +}); + +/** + * View Schema Definition + */ +const studioViewSchema = yup.object({ + name: pascalCaseAlphaNumString().required(), + id: yup.string().nullable(), + dataSource: yup.object({ + identifiers: yup.array().nullable(), + model: yup.string().nullable(), + predicate: yup.object().nullable(), + sort: yup.array().nullable(), + type: yup.string().matches(new RegExp('(DataStore|Custom)')), + }), + style: yup.object().nullable(), + viewConfiguration: yup.object().nullable(), +}); + /** * Studio Schema Validation Functions and Helpers. */ -const validateSchema = (validator: yup.AnySchema, studioSchema: any) => { +const validateSchema = (validator: yup.AnySchema, studioSchema: StudioSchema) => { try { validator.validateSync(studioSchema, { strict: true, abortEarly: false }); } catch (e) { @@ -193,3 +233,5 @@ const validateSchema = (validator: yup.AnySchema, studioSchema: any) => { export const validateComponentSchema = (schema: any) => validateSchema(studioComponentSchema, schema); export const validateThemeSchema = (schema: any) => validateSchema(studioThemeSchema, schema); +export const validateFormSchema = (schema: any) => validateSchema(studioFormSchema, schema); +export const validateViewSchema = (schema: any) => validateSchema(studioViewSchema, schema); diff --git a/packages/codegen-ui/package-lock.json b/packages/codegen-ui/package-lock.json index 4d5782481..df10e4c5d 100644 --- a/packages/codegen-ui/package-lock.json +++ b/packages/codegen-ui/package-lock.json @@ -6,9 +6,10 @@ "packages": { "": { "name": "@aws-amplify/codegen-ui", - "version": "2.3.1", + "version": "2.3.2", "license": "Apache-2.0", "dependencies": { + "change-case": "^4.1.2", "yup": "^0.32.11" }, "devDependencies": { @@ -4424,6 +4425,20 @@ "node": ">=4" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camel-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -4454,6 +4469,21 @@ ], "peer": true }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/capital-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4471,6 +4501,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/change-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -4784,6 +4838,21 @@ "node": ">= 0.10.0" } }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/constant-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -5022,6 +5091,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5808,6 +5891,20 @@ "node": ">=0.10.0" } }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/header-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/hermes-engine": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", @@ -6855,6 +6952,19 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -7511,6 +7621,20 @@ "dev": true, "peer": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/no-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/nocache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", @@ -7986,6 +8110,20 @@ "integrity": "sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==", "dev": true }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -8010,6 +8148,20 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -8020,6 +8172,20 @@ "node": ">=0.10.0" } }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8855,6 +9021,21 @@ "node": ">= 0.8" } }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/sentence-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/serialize-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", @@ -9058,6 +9239,20 @@ "dev": true, "peer": true }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/snake-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -9940,6 +10135,32 @@ "node": ">=0.10.0" } }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/upper-case/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -13782,6 +14003,22 @@ "dev": true, "peer": true }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -13796,6 +14033,23 @@ "dev": true, "peer": true }, + "capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -13807,6 +14061,32 @@ "supports-color": "^7.1.0" } }, + "change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "requires": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -14071,6 +14351,23 @@ "utils-merge": "1.0.1" } }, + "constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -14266,6 +14563,22 @@ "dev": true, "peer": true }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -14877,6 +15190,22 @@ } } }, + "header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "requires": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "hermes-engine": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", @@ -15728,6 +16057,21 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -16291,6 +16635,22 @@ "dev": true, "peer": true }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "nocache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", @@ -16658,6 +17018,22 @@ "integrity": "sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==", "dev": true }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -16676,6 +17052,22 @@ "dev": true, "peer": true }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -16683,6 +17075,22 @@ "dev": true, "peer": true }, + "path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -17372,6 +17780,23 @@ } } }, + "sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "serialize-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", @@ -17546,6 +17971,22 @@ } } }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -18270,6 +18711,36 @@ } } }, + "upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, + "upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", diff --git a/packages/codegen-ui/package.json b/packages/codegen-ui/package.json index 56f2d4632..47a8b0014 100644 --- a/packages/codegen-ui/package.json +++ b/packages/codegen-ui/package.json @@ -19,6 +19,7 @@ "build:watch": "npm run build -- --watch" }, "dependencies": { + "change-case": "^4.1.2", "yup": "^0.32.11" }, "devDependencies": { diff --git a/packages/test-generator/CHANGELOG.md b/packages/test-generator/CHANGELOG.md index bb406a7bd..1b64130c5 100644 --- a/packages/test-generator/CHANGELOG.md +++ b/packages/test-generator/CHANGELOG.md @@ -7,28 +7,15 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline **Note:** Version bump only for package @aws-amplify/codegen-ui-test-generator - - - - ## [2.3.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.3.0...v2.3.1) (2022-07-15) **Note:** Version bump only for package @aws-amplify/codegen-ui-test-generator - - - - # [2.3.0](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.1...v2.3.0) (2022-07-14) - ### Bug Fixes -* handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) - - - - +- handle auth prop in concat ([f7d645e](https://github.com/aws-amplify/amplify-codegen-ui/commit/f7d645e07e91848465e92f450e81d6ed92604057)) ## [2.2.1](https://github.com/aws-amplify/amplify-codegen-ui/compare/v2.2.0...v2.2.1) (2022-06-15) diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/complex-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/complex-spec.cy.ts index 6704b5f7e..1d3beb868 100644 --- a/packages/test-generator/integration-test-templates/cypress/e2e/complex-spec.cy.ts +++ b/packages/test-generator/integration-test-templates/cypress/e2e/complex-spec.cy.ts @@ -244,7 +244,6 @@ describe('Complex Components', () => { .then((el) => { const style = el.attr('style'); const expectedStyles = [ - 'transition: all 0.25s ease 0s', 'align-items: flex-start', 'background-color: rgb(255, 255, 255)', 'flex-direction: row', diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts new file mode 100644 index 000000000..6303ee221 --- /dev/null +++ b/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts @@ -0,0 +1,82 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +describe('Forms', () => { + before(() => { + cy.visit('http://localhost:3000/form-tests'); + }); + + it('CustomFormCreateDog', () => { + const ErrorMessageMap = { + name: 'Name must be longer than 1 character', + age: 'Age must be greater than 0', + validEmail: 'The value must be a valid email address', + customValidation: 'All dog emails are yahoo emails', + ip: 'The value must be an IPv4 or IPv6 address', + }; + cy.get('#customFormCreateDog').within(() => { + const blurField = () => cy.contains('Register your dog').click(); + + // should not submit if required field empty + cy.contains('Submit').click(); + cy.contains('submitted: false'); + + // validates email + cy.get('input').eq(2).type('fdfdsfd'); + // does not validate onChange if no error + cy.contains(ErrorMessageMap.validEmail).should('not.exist'); + // validates on blur + blurField(); + cy.contains(ErrorMessageMap.validEmail); + // validates onChange if error + cy.get('input').eq(2).type('jd@yahoo.com'); + cy.contains(ErrorMessageMap.validEmail).should('not.exist'); + + cy.contains('Clear').click(); + + // validates on blur & extends with onValidate prop + cy.get('input').eq(0).type('S'); + blurField(); + cy.get('input').eq(1).type('-1'); + blurField(); + cy.get('input').eq(2).type('spot@gmail.com'); + blurField(); + cy.get('input').eq(3).type('invalid ip'); + blurField(); + cy.contains(ErrorMessageMap.name); + cy.contains(ErrorMessageMap.age); + cy.contains(ErrorMessageMap.validEmail).should('not.exist'); + cy.contains(ErrorMessageMap.customValidation); + cy.contains(ErrorMessageMap.ip); + + // clears and submits + cy.contains('Clear').click(); + cy.get('input').eq(0).type('Spot'); + blurField(); + cy.get('input').eq(1).type('3'); + blurField(); + cy.get('input').eq(2).type('spot@yahoo.com'); + blurField(); + cy.get('input').eq(3).type('192.0.2.146'); + blurField(); + cy.contains('Submit').click(); + cy.contains('submitted: true'); + cy.contains('name: Spot'); + cy.contains('age: 3'); + cy.contains('email: spot@yahoo.com'); + cy.contains('ip: 192.0.2.146'); + }); + }); +}); diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts index 6e086930e..19eadc3b1 100644 --- a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts +++ b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts @@ -28,6 +28,7 @@ const EXPECTED_SUCCESSFUL_CASES = new Set([ 'BasicComponentImage', 'BasicComponentText', 'BasicComponentCustomRating', + 'CustomFormCreateDog', 'ComponentWithDataBindingWithPredicate', 'ComponentWithDataBindingWithoutPredicate', 'ComponentWithSimplePropertyBinding', @@ -102,6 +103,7 @@ const EXPECTED_SUCCESSFUL_CASES = new Set([ 'ToggleButtonGroupPrimitive', 'ViewPrimitive', 'VisuallyHiddenPrimitive', + 'ComponentWithBreakpoint', 'ComponentWithVariant', 'ComponentWithVariantAndOverrides', 'ComponentWithVariantsAndNotOverrideChildProp', diff --git a/packages/test-generator/integration-test-templates/src/App.tsx b/packages/test-generator/integration-test-templates/src/App.tsx index e8f0f8356..a394b2bfb 100644 --- a/packages/test-generator/integration-test-templates/src/App.tsx +++ b/packages/test-generator/integration-test-templates/src/App.tsx @@ -23,6 +23,7 @@ import SnippetTests from './SnippetTests'; // eslint-disable-line import/extensi import WorkflowTests from './WorkflowTests'; import TwoWayBindingTests from './TwoWayBindingTests'; import ActionBindingTests from './ActionBindingTests'; +import FormTests from './FormTests'; import { DATA_STORE_MOCK_EXPORTS, AUTH_MOCK_EXPORTS } from './mock-utils'; // use fake endpoint so useDataStoreBinding does not fail @@ -60,6 +61,9 @@ const HomePage = () => {
  • Action Binding Test
  • +
  • + Form Tests +
  • ); @@ -77,6 +81,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/packages/test-generator/integration-test-templates/src/FormTests.tsx b/packages/test-generator/integration-test-templates/src/FormTests.tsx new file mode 100644 index 000000000..1783da0da --- /dev/null +++ b/packages/test-generator/integration-test-templates/src/FormTests.tsx @@ -0,0 +1,51 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import '@aws-amplify/ui-react/styles.css'; +import { AmplifyProvider, View, Heading, Divider, Text } from '@aws-amplify/ui-react'; +import { useState } from 'react'; +import { CustomFormCreateDog } from './ui-components'; // eslint-disable-line import/extensions + +export default function FormTests() { + const [customFormCreateDogSubmitResults, setCustomFormCreateDogSubmitResults] = useState({}); + return ( + + Custom Form - CreateDog + + setCustomFormCreateDogSubmitResults(r)} + onValidate={{ + email: (value, validationResponse) => { + if (validationResponse.hasError) { + return validationResponse; + } + if (!value?.includes('yahoo.com')) { + return { hasError: true, errorMessage: 'All dog emails are yahoo emails' }; + } + return { hasError: false }; + }, + }} + /> + {`submitted: ${!!Object.keys(customFormCreateDogSubmitResults).length}`} + {`name: ${customFormCreateDogSubmitResults.name}`} + {`name: ${customFormCreateDogSubmitResults.name}`} + {`age: ${customFormCreateDogSubmitResults.age}`} + {`email: ${customFormCreateDogSubmitResults.email}`} + {`ip: ${customFormCreateDogSubmitResults.ip}`} + + + + ); +} diff --git a/packages/test-generator/lib/components/variants/componentWithBreakpoint.json b/packages/test-generator/lib/components/variants/componentWithBreakpoint.json new file mode 100644 index 000000000..40f0d8c7d --- /dev/null +++ b/packages/test-generator/lib/components/variants/componentWithBreakpoint.json @@ -0,0 +1,33 @@ +{ + "id": "1234-5678-9010", + "componentType": "Button", + "name": "ComponentWithBreakpoint", + "properties": { + "children": { + "value": "ComponentWithBreakpoint" + } + }, + "variants": [ + { + "variantValues": { + "breakpoint": "small" + }, + "overrides": { + "ComponentWithVariant": { + "fontSize": "12px" + } + } + }, + { + "variantValues": { + "breakpoint": "medium" + }, + "overrides": { + "ComponentWithVariant": { + "fontSize": "40px" + } + } + } + ], + "schemaVersion": "1.0" +} diff --git a/packages/test-generator/lib/components/variants/index.ts b/packages/test-generator/lib/components/variants/index.ts index ade20bf70..76ca6ec67 100644 --- a/packages/test-generator/lib/components/variants/index.ts +++ b/packages/test-generator/lib/components/variants/index.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +export { default as ComponentWithBreakpoint } from './componentWithBreakpoint.json'; export { default as ComponentWithVariant } from './componentWithVariant.json'; export { default as ComponentWithVariantAndOverrides } from './componentWithVariantAndOverrides.json'; // eslint-disable-next-line max-len diff --git a/packages/test-generator/lib/forms/custom-form-create-dog.json b/packages/test-generator/lib/forms/custom-form-create-dog.json new file mode 100644 index 000000000..86d0852d5 --- /dev/null +++ b/packages/test-generator/lib/forms/custom-form-create-dog.json @@ -0,0 +1,48 @@ +{ + "id": "123", + "name": "CustomFormCreateDog", + "formActionType": "create", + "dataType": { + "dataSourceType": "Custom", + "dataTypeName": "Dog" + }, + "fields": { + "name": { + "label": "Name", + "inputType": { + "type": "TextField" + }, + "validations": [{"type": "GreaterThanChar", "numValues": ["1"], "validationMessage": "Name must be longer than 1 character"}] + }, + "age": { + "label": "Age", + "inputType": { + "type": "NumberField" + }, + "validations": [{"type": "GreaterThanNum","numValues": ["0"], "validationMessage": "Age must be greater than 0"}] + }, + "email": { + "label": "Email", + "inputType": { + "type": "EmailField" + } + }, + "ip": { + "label": "IP Address", + "inputType": { + "type": "IPAddressField", + "required": true + } + } + }, + "sectionalElements": { + "formHeading": { + "type": "Heading", + "position": {"fixed": "first"}, + "text": "Register your dog" + } + }, + "style": {}, + "cta": {}, + "schemaVersion": "1.0" +} \ No newline at end of file diff --git a/packages/test-generator/lib/forms/index.ts b/packages/test-generator/lib/forms/index.ts new file mode 100644 index 000000000..9192eccca --- /dev/null +++ b/packages/test-generator/lib/forms/index.ts @@ -0,0 +1,17 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +export { default as CustomFormCreateDog } from './custom-form-create-dog.json'; diff --git a/packages/test-generator/lib/generators/BrowserTestGenerator.ts b/packages/test-generator/lib/generators/BrowserTestGenerator.ts index 09bacde59..b5d83a67f 100644 --- a/packages/test-generator/lib/generators/BrowserTestGenerator.ts +++ b/packages/test-generator/lib/generators/BrowserTestGenerator.ts @@ -15,12 +15,22 @@ */ /* Test Generator to be used in the browser environment */ -import { StudioComponent, StudioTheme } from '@aws-amplify/codegen-ui'; +import { + FormMetadata, + getGenericFromDataStore, + StudioComponent, + StudioForm, + StudioTheme, +} from '@aws-amplify/codegen-ui'; import { AmplifyRenderer, ReactThemeStudioTemplateRenderer, ReactIndexStudioTemplateRenderer, + ReactUtilsStudioTemplateRenderer, + AmplifyFormRenderer, + UtilTemplateType, } from '@aws-amplify/codegen-ui-react'; +import schema from '../models/schema'; import { TestGenerator } from './TestGenerator'; export class BrowserTestGenerator extends TestGenerator { @@ -28,18 +38,32 @@ export class BrowserTestGenerator extends TestGenerator { writeThemeToDisk() {} // no-op + writeFormToDisk() { + return { formMetadata: {} as FormMetadata }; + } // no-op + writeIndexFileToDisk() {} // no-op + writeUtilsFileToDisk() {} // no-op + writeSnippetToDisk() {} // no-op - renderIndexFile(schemas: (StudioComponent | StudioTheme)[]) { + renderIndexFile(schemas: (StudioComponent | StudioForm | StudioTheme)[]) { return new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig).renderComponent(); } + renderUtilsFile(utils: UtilTemplateType[]) { + return new ReactUtilsStudioTemplateRenderer(utils, this.renderConfig).renderComponent(); + } + renderComponent(component: StudioComponent) { return new AmplifyRenderer(component, this.renderConfig).renderComponentOnly(); } + renderForm(form: StudioForm) { + return new AmplifyFormRenderer(form, getGenericFromDataStore(schema), this.renderConfig).renderComponentOnly(); + } + renderTheme(theme: StudioTheme) { return new ReactThemeStudioTemplateRenderer(theme, this.renderConfig).renderComponent(); } diff --git a/packages/test-generator/lib/generators/NodeTestGenerator.ts b/packages/test-generator/lib/generators/NodeTestGenerator.ts index 4730fb334..1cdca03d5 100644 --- a/packages/test-generator/lib/generators/NodeTestGenerator.ts +++ b/packages/test-generator/lib/generators/NodeTestGenerator.ts @@ -22,12 +22,19 @@ import { StudioTemplateRendererFactory, StudioComponent, StudioTheme, + StudioForm, + getGenericFromDataStore, + StudioSchema, } from '@aws-amplify/codegen-ui'; import { AmplifyRenderer, ReactThemeStudioTemplateRenderer, ReactIndexStudioTemplateRenderer, + ReactUtilsStudioTemplateRenderer, + AmplifyFormRenderer, + UtilTemplateType, } from '@aws-amplify/codegen-ui-react'; +import schema from '../models/schema'; import { TestGenerator, TestGeneratorParams } from './TestGenerator'; export class NodeTestGenerator extends TestGenerator { @@ -35,14 +42,22 @@ export class NodeTestGenerator extends TestGenerator { private readonly themeRendererFactory: any; + private readonly formRendererFactory: any; + private readonly indexRendererFactory: any; - private readonly rendererManager: any; + private readonly utilsRendererFactory: any; + + private readonly componentRendererManager: any; + + private readonly formRendererManager: any; private readonly themeRendererManager: any; private readonly indexRendererManager: any; + private readonly utilsRendererManager: any; + constructor(params: TestGeneratorParams) { super(params); this.componentRendererFactory = new StudioTemplateRendererFactory( @@ -51,16 +66,25 @@ export class NodeTestGenerator extends TestGenerator { this.themeRendererFactory = new StudioTemplateRendererFactory( (theme: StudioTheme) => new ReactThemeStudioTemplateRenderer(theme, this.renderConfig), ); + + this.formRendererFactory = new StudioTemplateRendererFactory( + (form: StudioForm) => new AmplifyFormRenderer(form, getGenericFromDataStore(schema), this.renderConfig), + ); this.indexRendererFactory = new StudioTemplateRendererFactory( - (schemas: (StudioComponent | StudioTheme)[]) => new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig), + (schemas: StudioSchema[]) => new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig), ); - this.rendererManager = new StudioTemplateRendererManager(this.componentRendererFactory, this.outputConfig); + this.utilsRendererFactory = new StudioTemplateRendererFactory( + (utils: UtilTemplateType[]) => new ReactUtilsStudioTemplateRenderer(utils, this.renderConfig), + ); + this.componentRendererManager = new StudioTemplateRendererManager(this.componentRendererFactory, this.outputConfig); + this.formRendererManager = new StudioTemplateRendererManager(this.formRendererFactory, this.outputConfig); this.themeRendererManager = new StudioTemplateRendererManager(this.themeRendererFactory, this.outputConfig); this.indexRendererManager = new StudioTemplateRendererManager(this.indexRendererFactory, this.outputConfig); + this.utilsRendererManager = new StudioTemplateRendererManager(this.utilsRendererFactory, this.outputConfig); } writeComponentToDisk(component: StudioComponent) { - this.rendererManager.renderSchemaToTemplate(component); + this.componentRendererManager.renderSchemaToTemplate(component); } renderComponent(component: StudioComponent) { @@ -68,6 +92,15 @@ export class NodeTestGenerator extends TestGenerator { return buildRenderer.renderComponentOnly(); } + writeFormToDisk(form: StudioForm) { + return this.formRendererManager.renderSchemaToTemplate(form); + } + + renderForm(form: StudioForm) { + const buildRenderer = this.formRendererFactory.buildRenderer(form); + return buildRenderer.renderComponentOnly(); + } + writeSnippetToDisk(components: StudioComponent[]) { const { importsText, compText } = this.renderSnippet(components); fs.writeFileSync(path.join(this.outputConfig.outputPathDir, '..', 'SnippetTests.jsx'), importsText + compText); @@ -99,12 +132,21 @@ export class NodeTestGenerator extends TestGenerator { return buildRenderer.renderComponent(); } - writeIndexFileToDisk(schemas: (StudioComponent | StudioTheme)[]) { + writeIndexFileToDisk(schemas: (StudioComponent | StudioTheme | StudioForm)[]) { this.indexRendererManager.renderSchemaToTemplate(schemas); } - renderIndexFile(schemas: (StudioComponent | StudioTheme)[]) { + renderIndexFile(schemas: (StudioComponent | StudioTheme | StudioForm)[]) { const indexRenderer = this.indexRendererFactory.buildRenderer(schemas); return indexRenderer.renderComponent(); } + + writeUtilsFileToDisk(utils: UtilTemplateType[]) { + this.utilsRendererManager.renderSchemaToTemplate(utils); + } + + renderUtilsFile(utils: UtilTemplateType[]) { + const utilsRenderer = this.utilsRendererFactory.buildRenderer(utils); + return utilsRenderer.renderComponent(); + } } diff --git a/packages/test-generator/lib/generators/TestGenerator.ts b/packages/test-generator/lib/generators/TestGenerator.ts index b74d9a731..8d6f08587 100644 --- a/packages/test-generator/lib/generators/TestGenerator.ts +++ b/packages/test-generator/lib/generators/TestGenerator.ts @@ -13,17 +13,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StudioComponent, StudioTheme } from '@aws-amplify/codegen-ui'; +import { FormMetadata, StudioComponent, StudioForm, StudioTheme } from '@aws-amplify/codegen-ui'; import { ModuleKind, ScriptTarget, ScriptKind, ReactRenderConfig, ReactOutputConfig, + UtilTemplateType, } from '@aws-amplify/codegen-ui-react'; import log from 'loglevel'; import * as ComponentSchemas from '../components'; import * as ThemeSchemas from '../themes'; +import * as FormSchemas from '../forms'; const DEFAULT_RENDER_CONFIG = { module: ModuleKind.CommonJS, @@ -40,7 +42,7 @@ log.setLevel('info'); export type TestCase = { name: string; schema: any; - testType: 'Component' | 'Theme' | 'Snippet'; + testType: 'Component' | 'Theme' | 'Form' | 'Snippet'; }; export type TestGeneratorParams = { @@ -66,6 +68,7 @@ export abstract class TestGenerator { generate = (testCases: TestCase[]) => { const renderErrors: { [key: string]: any } = {}; + const utilsFunctions = new Set(); const generateComponent = (testCase: TestCase) => { const { name, schema } = testCase; @@ -112,6 +115,34 @@ export abstract class TestGenerator { } }; + const generateForm = (testCase: TestCase) => { + const { name, schema } = testCase; + try { + if (this.params.writeToDisk) { + const res = this.writeFormToDisk(schema as StudioForm); + if (res.formMetadata?.fieldConfigs && Object.keys(res.formMetadata.fieldConfigs).length) { + utilsFunctions.add('validation'); + utilsFunctions.add('fetchByPath'); + } + } + + if (this.params.writeToLogger) { + const { importsText, compText } = this.renderForm(schema as StudioForm); + log.info(`# ${name}`); + log.info('## Form Only Output'); + log.info('### formImports'); + log.info(this.decorateTypescriptWithMarkdown(importsText)); + log.info('### formText'); + log.info(this.decorateTypescriptWithMarkdown(compText)); + } + } catch (err) { + if (this.params.immediatelyThrowGenerateErrors) { + throw err; + } + renderErrors[name] = err; + } + }; + const generateIndexFile = (indexFileTestCases: TestCase[]) => { const schemas = indexFileTestCases.map((testCase) => testCase.schema); try { @@ -131,6 +162,24 @@ export abstract class TestGenerator { } }; + const generateUtilsFile = (utils: UtilTemplateType[]) => { + try { + if (this.params.writeToDisk) { + this.writeUtilsFileToDisk(utils); + } + if (this.params.writeToLogger) { + const utilsFile = this.renderUtilsFile(utils); + log.info(`# utils`); + log.info(this.decorateTypescriptWithMarkdown(utilsFile.componentText)); + } + } catch (err) { + if (this.params.immediatelyThrowGenerateErrors) { + throw err; + } + renderErrors.index = err; + } + }; + const generateSnippet = (snippetTestCases: TestCase[]) => { const components = snippetTestCases.map((testCase) => testCase.schema); try { @@ -162,16 +211,23 @@ export abstract class TestGenerator { case 'Theme': generateTheme(testCase); break; + case 'Form': + generateForm(testCase); + break; case 'Snippet': generateSnippet([testCase]); break; default: - throw new Error('Expected either a `Component` or `Theme` test case type'); + throw new Error('Expected either a `Component`, `Theme`, `Form` test case type'); } }); generateIndexFile(testCases); + if (utilsFunctions.size) { + generateUtilsFile([...utilsFunctions]); + } + // only test with 4 components for performance generateSnippet(testCases.filter((testCase) => testCase.testType === 'Component').slice(0, 4)); @@ -193,13 +249,21 @@ export abstract class TestGenerator { abstract writeThemeToDisk(theme: StudioTheme): void; + abstract writeFormToDisk(form: StudioForm): { formMetadata: FormMetadata }; + abstract renderComponent(component: StudioComponent): { compText: string; importsText: string }; abstract renderTheme(theme: StudioTheme): { componentText: string }; - abstract writeIndexFileToDisk(schemas: (StudioComponent | StudioTheme)[]): void; + abstract renderForm(form: StudioForm): { compText: string; importsText: string }; - abstract renderIndexFile(schemas: (StudioComponent | StudioTheme)[]): { componentText: string }; + abstract writeIndexFileToDisk(schemas: (StudioComponent | StudioForm | StudioTheme)[]): void; + + abstract renderIndexFile(schemas: (StudioComponent | StudioForm | StudioTheme)[]): { componentText: string }; + + abstract writeUtilsFileToDisk(utils: string[]): void; + + abstract renderUtilsFile(utils: string[]): { componentText: string }; abstract writeSnippetToDisk(components: StudioComponent[]): void; @@ -214,6 +278,9 @@ export abstract class TestGenerator { ...Object.entries(ThemeSchemas).map(([name, schema]) => { return { name, schema, testType: 'Theme' } as TestCase; }), + ...Object.entries(FormSchemas).map(([name, schema]) => { + return { name, schema, testType: 'Form' } as TestCase; + }), ].filter((testCase) => !disabledSchemaSet.has(testCase.name)); } } diff --git a/packages/test-generator/lib/index.ts b/packages/test-generator/lib/index.ts index 94edeb352..28c71e4b9 100644 --- a/packages/test-generator/lib/index.ts +++ b/packages/test-generator/lib/index.ts @@ -16,3 +16,4 @@ export * from './components'; export * from './generators'; export * from './themes'; +export * from './forms'; diff --git a/packages/test-generator/lib/models/schema.ts b/packages/test-generator/lib/models/schema.ts new file mode 100644 index 000000000..a03ca7c9a --- /dev/null +++ b/packages/test-generator/lib/models/schema.ts @@ -0,0 +1,364 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { Schema } from '@aws-amplify/codegen-ui'; + +export default { + models: { + UserPreference: { + name: 'UserPreference', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + favoriteColor: { + name: 'favoriteColor', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'UserPreferences', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + }, + User: { + name: 'User', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + firstName: { + name: 'firstName', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + lastName: { + name: 'lastName', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + age: { + name: 'age', + isArray: false, + type: 'Int', + isRequired: false, + attributes: [], + }, + isLoggedIn: { + name: 'isLoggedIn', + isArray: false, + type: 'Boolean', + isRequired: false, + attributes: [], + }, + loggedInColor: { + name: 'loggedInColor', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + loggedOutColor: { + name: 'loggedOutColor', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'Users', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + }, + Listing: { + name: 'Listing', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + title: { + name: 'title', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + priceUSD: { + name: 'priceUSD', + isArray: false, + type: 'Int', + isRequired: false, + attributes: [], + }, + description: { + name: 'description', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'Listings', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + }, + ComplexModel: { + name: 'ComplexModel', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + listElement: { + name: 'listElement', + isArray: true, + type: 'String', + isRequired: true, + attributes: [], + isArrayNullable: false, + }, + myCustomField: { + name: 'myCustomField', + isArray: false, + type: { + nonModel: 'CustomType', + }, + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'ComplexModels', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + }, + Class: { + name: 'Class', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + name: { + name: 'name', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + }, + syncable: true, + pluralName: 'Classes', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + }, + }, + enums: {}, + nonModels: { + CustomType: { + name: 'CustomType', + fields: { + StringVal: { + name: 'StringVal', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + NumVal: { + name: 'NumVal', + isArray: false, + type: 'Int', + isRequired: false, + attributes: [], + }, + BoolVal: { + name: 'BoolVal', + isArray: false, + type: 'Boolean', + isRequired: false, + attributes: [], + }, + }, + }, + }, + version: 'f6252c821249b6b1abda9fb24481c5a4', +} as Schema; diff --git a/packages/test-generator/package-lock.json b/packages/test-generator/package-lock.json index 5529d291b..d0583e2d2 100644 --- a/packages/test-generator/package-lock.json +++ b/packages/test-generator/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@aws-amplify/codegen-ui-test-generator", - "version": "2.3.1", + "version": "2.3.2", "license": "Apache-2.0", "dependencies": { "@types/node": "^15.12.1",