From eda8f5e69d363e86817e86904f221d87be24f09b Mon Sep 17 00:00:00 2001 From: Christopher Froehlich Date: Thu, 12 May 2022 16:59:28 -0400 Subject: [PATCH] feat(console): squashed all branch commits > new --- .npmrc | 1 - .pnp.cjs | 43 - .prettierignore | 5 - .prettierrc.yml | 7 - .vscode/extensions.json | 6 +- .vscode/launch.json | 4 +- package.json | 2 +- packages/questionable-build/package.json | 1 - .../packages_still_in_dist.txt | 61 ++ .../questionable-build/src/parseSchema.ts | 14 +- .../src/composable/Iterable.ts | 261 ++--- .../src/composable/PromptFactory.ts | 71 ++ .../src/composable/Question.ts | 75 ++ .../src/composable/Questionnaire.ts | 34 +- .../src/composable/Step.ts | 22 + .../src/examples/Scaffolding.ts | 29 + .../src/examples/index.ts | 1 + .../src/examples/scaffolding/onboarding.ts | 330 +++++++ packages/questionable-console/src/index.ts | 20 +- packages/questionable-console/src/s.ts | 29 + .../src/survey/IQuestionnaire.ts | 7 - .../questionable-console/src/survey/IStep.ts | 23 - .../questionable-console/src/util/helper.ts | 36 + .../questionable-console/src/util/inquirer.ts | 15 + .../questionable-console/src/util/types.ts | 35 + packages/questionable-core/package.json | 3 +- .../src/composable/ActionCore.ts | 97 ++ .../src/composable/AnswerCore.ts | 86 ++ .../src/composable/BaseCore.ts | 92 ++ .../src/composable/BranchCore.ts | 76 ++ .../src/composable/ButtonCore.ts | 80 ++ .../src/composable/ConfigCore.ts | 193 ++++ .../src/composable/Dictionary.ts | 49 + .../src/composable/EventEmitterCore.ts | 81 +- .../src/composable/FormCore.ts | 146 ++- .../src/composable/PageCore.ts | 83 ++ .../src/composable/PagesCore.ts | 152 +++ .../src/composable/QuestionCore.ts | 118 +++ .../src/composable/QuestionableConfigCore.ts | 129 --- .../src/composable/QuestionnaireCore.ts | 794 +++------------- .../src/composable/RefCore.ts | 125 +++ .../src/composable/RequirementCore.ts | 96 ++ .../src/composable/ResponseCore.ts | 90 ++ .../src/composable/ResultCore.ts | 135 +++ .../src/composable/SectionCore.ts | 102 ++ .../src/composable/StepCore.ts | 157 ++++ .../questionable-core/src/composable/TCtor.ts | 17 + .../src/composable/_exports.ts | 21 + .../src/composable/config/ButtonConfig.ts | 87 ++ .../src/composable/config/NavigationConfig.ts | 48 + .../src/composable/config/PagesConfig.ts | 30 + .../composable/config/ProgressBarConfig.ts | 75 ++ .../src/composable/config/QuestionConfig.ts | 38 + .../src/composable/config/StepConfig.ts | 55 ++ .../src/composable/config/_exports.ts | 6 + .../questionable-core/src/composable/index.ts | 4 - .../src/constructable/Factory.ts | 237 +++++ .../src/constructable/GateLogicCore.ts} | 529 ++++++----- .../src/constructable/Questioner.ts | 63 ++ .../src/constructable/Questionnaire.ts | 82 ++ .../src/constructable/ReferentialIntegrity.ts | 19 + .../src/constructable/SurveyBuilderCore.ts | 207 ++++ .../src/constructable/_exports.ts | 4 + .../src/constructable/lib/TQForm.ts | 7 + .../src/constructable/lib/TSForm.ts | 7 + .../src/constructable/lib/defaultReducer.ts | 64 ++ .../src/constructable/lib/getBirthdate.ts | 23 + .../src/constructable/lib/isSelected.ts | 20 + .../src/constructable/lib/isValid.ts | 38 + .../src/constructable/lib/pools.ts | 65 ++ .../src/constructable/lib/toBirthdate.ts | 30 + .../src/constructable/lib/toString.ts | 8 + .../src/constructable/lib/updateForm.ts | 37 + .../src/constructable/lib/validators.ts | 39 + packages/questionable-core/src/index.ts | 12 +- .../src/lib/QuestionsCore.ts | 114 --- .../src/{util => lib}/README.md | 0 .../questionable-core/src/lib/StepsCore.ts | 116 --- .../src/{util/index.ts => lib/_exports.ts} | 7 +- .../src/{util => lib}/array.ts | 5 +- .../src/{util => lib}/date.ts | 32 +- packages/questionable-core/src/lib/enums.ts | 26 + .../src/{util => lib}/error.ts | 4 +- .../questionable-core/src/lib/factories.ts | 88 ++ .../src/{util => lib}/helpers.ts | 0 packages/questionable-core/src/lib/index.ts | 2 - .../questionable-core/src/lib/instanceOf.ts | 267 ++++++ packages/questionable-core/src/lib/labels.ts | 89 ++ packages/questionable-core/src/lib/logger.ts | 63 ++ packages/questionable-core/src/lib/merge.ts | 72 ++ .../src/{util => lib}/noop.ts | 7 +- packages/questionable-core/src/lib/set.ts | 31 + packages/questionable-core/src/lib/types.ts | 11 + packages/questionable-core/src/lib/uuid.ts | 11 + .../src/{survey => metadata}/IActionCore.ts | 8 +- .../src/metadata/IAnswerCore.ts | 8 + .../src/metadata/IBranchCore.ts | 10 + .../src/{survey => metadata}/IButtonCore.ts | 8 +- .../src/metadata/IConfigCore.ts | 76 ++ .../src/metadata/IEventCore.ts | 18 + .../src/{survey => metadata}/IFormCore.ts | 8 +- .../src/metadata/IPageCore.ts | 33 + .../src/{survey => metadata}/IPagesCore.ts | 9 +- .../src/metadata/IQuestionCore.ts | 36 + .../src/metadata/IQuestionnaireCore.ts | 21 + .../src/{survey => metadata}/IRefCore.ts | 12 +- .../src/metadata/IRequirementCore.ts | 42 + .../src/metadata/IResponseCore.ts | 13 + .../src/{survey => metadata}/IResultCore.ts | 13 +- .../src/metadata/ISectionCore.ts | 32 + .../src/metadata/IStepCore.ts | 66 ++ .../src/{survey => metadata}/README.md | 2 +- .../{survey/index.ts => metadata/_exports.ts} | 14 +- .../src/metadata/properties/MAction.ts | 20 + .../src/metadata/properties/MAnswer.ts | 16 + .../src/metadata/properties/MBase.ts | 11 + .../src/metadata/properties/MBranch.ts | 19 + .../src/metadata/properties/MCommon.ts | 14 + .../src/metadata/properties/MConfig.ts | 34 + .../src/metadata/properties/MEvent.ts | 33 + .../src/metadata/properties/MForm.ts | 24 + .../src/metadata/properties/MPage.ts | 22 + .../src/metadata/properties/MPages.ts | 22 + .../src/metadata/properties/MQuestion.ts | 23 + .../src/metadata/properties/MQuestionnaire.ts | 34 + .../src/metadata/properties/MRef.ts | 24 + .../src/metadata/properties/MRequirement.ts | 23 + .../src/metadata/properties/MResponse.ts | 18 + .../src/metadata/properties/MResult.ts | 31 + .../src/metadata/properties/MSection.ts | 23 + .../src/metadata/properties/MStep.ts | 31 + .../src/metadata/properties/README.md | 8 + .../src/metadata/properties/_exports.ts | 19 + .../metadata/properties/type/TActionType.ts | 18 + .../metadata/properties/type/TAnswerType.ts | 12 + .../src/metadata/properties/type/TBaseType.ts | 4 + .../metadata/properties/type/TBranchType.ts | 12 + .../metadata/properties/type/TButtonType.ts | 12 + .../src/metadata/properties/type/TPageType.ts | 20 + .../metadata/properties/type/TQuestionType.ts | 24 + .../src/metadata/properties/type/TRefType.ts | 30 + .../properties/type/TRequirementType.ts | 12 + .../metadata/properties/type/TResponseType.ts | 12 + .../metadata/properties/type/TResultType.ts | 12 + .../metadata/properties/type/TSectionType.ts | 12 + .../src/metadata/properties/type/TStepType.ts | 15 + .../src/metadata/properties/type/_exports.ts | 13 + .../src/metadata/types/ClassProperties.ts | 16 + .../src/metadata/types/TAgeCore.ts | 33 + .../src/metadata/types/TAnswerDataCore.ts | 16 + .../src/metadata/types/TCollectable.ts | 8 + .../src/metadata/types/TContentCore.ts | 20 + .../src/metadata/types/TDesignType.ts | 10 + .../src/metadata/types/TGateCore.ts | 57 ++ .../metadata/types/THorizontalPositionCore.ts | 8 + .../src/metadata/types/TOpType.ts | 13 + .../src/metadata/types/TPageDataCore.ts | 14 + .../metadata/types/TProgressBarStatusType.ts | 14 + .../src/metadata/types/TReferentialble.ts | 27 + .../src/metadata/types/TResultDataCore.ts | 9 + .../metadata/types/TStringDictionaryCore.ts | 12 + .../src/metadata/types/TTypeable.ts | 7 + .../metadata/types/TVerticalPositionCore.ts | 8 + .../src/metadata/types/TVisible.ts | 3 + .../src/metadata/types/_exports.ts | 14 + .../src/schema/{index.ts => _exports.ts} | 0 .../src/schema/editStepSchema.ts | 20 +- .../questionable-core/src/schema/survey.ts | 888 ++++++++---------- .../questionable-core/src/state/_exports.ts | 1 + packages/questionable-core/src/state/index.ts | 1 - .../questionable-core/src/state/pubsub.ts | 69 ++ .../src/state/stepReducer.ts | 42 - .../src/survey/IBranchCore.ts | 5 - .../src/survey/IDesignDataCore.ts | 10 - .../src/survey/IEventCore.ts | 106 --- .../src/survey/IPageDataCore.ts | 9 - .../src/survey/IQuestionDataCore.ts | 11 - .../src/survey/IQuestionableConfigCore.ts | 120 --- .../src/survey/IQuestionnaireCore.ts | 20 - .../questionable-core/src/survey/IStepCore.ts | 204 ---- .../src/survey/IStepDataCore.ts | 33 - .../src/tests/Scaffolding.ts | 289 ++++++ packages/questionable-core/src/util/enums.ts | 76 -- packages/questionable-core/src/util/log.ts | 31 - packages/questionable-core/src/util/types.ts | 92 -- packages/questionable-core/tsconfig.json | 2 +- packages/questionable-mocks/typings.d.ts | 1 - .../questionable-react-component/package.json | 8 +- .../parseSchema.ts | 6 +- .../scripts/parseSchema.js | 10 - .../src/components/Questionable.tsx | 57 +- .../src/components/design/Edit.tsx | 15 +- .../components/factories/DesignFactory.tsx | 18 +- .../src/components/factories/PageFactory.tsx | 28 +- .../components/factories/ProgressFactory.tsx | 10 +- .../components/factories/QuestionFactory.tsx | 26 +- .../src/components/factories/StepFactory.tsx | 19 +- .../src/components/lib/Pages.tsx | 83 +- .../src/components/lib/Questions.tsx | 128 ++- .../src/components/lib/Steps.tsx | 9 +- .../src/components/lib/Wizard.tsx | 91 +- .../src/components/lib/types.ts | 20 + .../src/components/pages/LandingPage.tsx | 17 +- .../src/components/pages/NoResultsPage.tsx | 33 +- .../src/components/pages/ResultsPage.tsx | 41 +- .../src/components/pages/SummaryPage.tsx | 43 +- .../src/components/questions/DateOfBirth.tsx | 23 +- .../src/components/questions/MultiSelect.tsx | 24 +- .../components/questions/MultipleChoice.tsx | 28 +- .../questions/lib/DateOfBirthUtils.tsx | 52 +- .../src/components/wizard/Action.tsx | 20 +- .../src/components/wizard/Button.tsx | 103 +- .../src/components/wizard/DesignLayout.tsx | 16 +- .../src/components/wizard/DevPanel.tsx | 14 +- .../src/components/wizard/NavBar.tsx | 15 +- .../src/components/wizard/ProgressBar.tsx | 6 +- .../src/components/wizard/StepIndicator.tsx | 14 +- .../src/components/wizard/StepLayout.tsx | 32 +- .../src/composable/Buttons.ts | 38 + .../src/composable/EventEmitter.ts | 83 -- .../src/composable/Form.ts | 41 - .../src/composable/Page.ts | 16 + .../src/composable/PageData.ts | 9 + .../src/composable/Question.ts | 16 + .../src/composable/QuestionData.ts | 9 + .../src/composable/QuestionableConfig.ts | 180 ---- .../src/composable/Step.ts | 29 + .../src/composable/StepData.ts | 50 + .../src/composable/Wizard.ts | 9 + .../src/composable/config.ts | 105 +++ .../src/composable/index.ts | 11 +- .../questionable-react-component/src/index.ts | 3 - .../src/schema/editStepSchema.ts | 23 +- .../src/schema/survey.ts | 80 +- .../src/state/GlobalState.tsx | 23 +- .../src/state/index.ts | 1 - .../src/state/stepReducer.ts | 42 - .../src/survey/IAction.ts | 33 - .../src/survey/IBranch.ts | 6 - .../src/survey/IButton.ts | 49 - .../src/survey/IDesignData.ts | 12 - .../src/survey/IEvent.ts | 78 -- .../src/survey/IForm.ts | 38 - .../src/survey/IPageData.ts | 12 - .../src/survey/IPages.ts | 32 - .../src/survey/IQuestionData.ts | 12 - .../src/survey/IQuestionable.ts | 6 - .../src/survey/IQuestionableConfig.ts | 219 ----- .../src/survey/IQuestionnaire.ts | 20 - .../src/survey/IRef.ts | 26 - .../src/survey/IResult.ts | 53 -- .../src/survey/IStep.ts | 245 ----- .../src/survey/IStepData.ts | 49 - .../src/survey/README.md | 10 - .../src/survey/index.ts | 15 - .../flows/eligibility/logic/actions.flow.ts | 6 +- .../src/flows/simple/data/actions.flow.ts | 4 +- packages/questionable-tests/package.json | 2 +- packages/questionable-tests/src/App.test.tsx | 2 +- packages/questionable-tests/src/App.tsx | 96 ++ packages/ssa-eligibility/package.json | 9 +- packages/ssa-eligibility/src/App.tsx | 6 +- .../ssa-eligibility/src/{index.ts => main.ts} | 0 packages/ssa-eligibility/tests/setupTests.ts | 1 + types.d.ts | 17 - yarn.lock | 7 +- 266 files changed, 8335 insertions(+), 4745 deletions(-) delete mode 100644 .npmrc delete mode 100644 .prettierignore delete mode 100644 .prettierrc.yml create mode 100644 packages/questionable-build/packages_still_in_dist.txt create mode 100644 packages/questionable-console/src/composable/PromptFactory.ts create mode 100644 packages/questionable-console/src/composable/Question.ts create mode 100644 packages/questionable-console/src/composable/Step.ts create mode 100644 packages/questionable-console/src/examples/Scaffolding.ts create mode 100644 packages/questionable-console/src/examples/index.ts create mode 100644 packages/questionable-console/src/examples/scaffolding/onboarding.ts create mode 100644 packages/questionable-console/src/s.ts delete mode 100644 packages/questionable-console/src/survey/IQuestionnaire.ts delete mode 100644 packages/questionable-console/src/survey/IStep.ts create mode 100644 packages/questionable-console/src/util/helper.ts create mode 100644 packages/questionable-console/src/util/inquirer.ts create mode 100644 packages/questionable-console/src/util/types.ts create mode 100644 packages/questionable-core/src/composable/ActionCore.ts create mode 100644 packages/questionable-core/src/composable/AnswerCore.ts create mode 100644 packages/questionable-core/src/composable/BaseCore.ts create mode 100644 packages/questionable-core/src/composable/BranchCore.ts create mode 100644 packages/questionable-core/src/composable/ButtonCore.ts create mode 100644 packages/questionable-core/src/composable/ConfigCore.ts create mode 100644 packages/questionable-core/src/composable/Dictionary.ts create mode 100644 packages/questionable-core/src/composable/PageCore.ts create mode 100644 packages/questionable-core/src/composable/PagesCore.ts create mode 100644 packages/questionable-core/src/composable/QuestionCore.ts delete mode 100644 packages/questionable-core/src/composable/QuestionableConfigCore.ts create mode 100644 packages/questionable-core/src/composable/RefCore.ts create mode 100644 packages/questionable-core/src/composable/RequirementCore.ts create mode 100644 packages/questionable-core/src/composable/ResponseCore.ts create mode 100644 packages/questionable-core/src/composable/ResultCore.ts create mode 100644 packages/questionable-core/src/composable/SectionCore.ts create mode 100644 packages/questionable-core/src/composable/StepCore.ts create mode 100644 packages/questionable-core/src/composable/TCtor.ts create mode 100644 packages/questionable-core/src/composable/_exports.ts create mode 100644 packages/questionable-core/src/composable/config/ButtonConfig.ts create mode 100644 packages/questionable-core/src/composable/config/NavigationConfig.ts create mode 100644 packages/questionable-core/src/composable/config/PagesConfig.ts create mode 100644 packages/questionable-core/src/composable/config/ProgressBarConfig.ts create mode 100644 packages/questionable-core/src/composable/config/QuestionConfig.ts create mode 100644 packages/questionable-core/src/composable/config/StepConfig.ts create mode 100644 packages/questionable-core/src/composable/config/_exports.ts delete mode 100644 packages/questionable-core/src/composable/index.ts create mode 100644 packages/questionable-core/src/constructable/Factory.ts rename packages/{questionable-react-component/src/composable/Questionnaire.ts => questionable-core/src/constructable/GateLogicCore.ts} (56%) create mode 100644 packages/questionable-core/src/constructable/Questioner.ts create mode 100644 packages/questionable-core/src/constructable/Questionnaire.ts create mode 100644 packages/questionable-core/src/constructable/ReferentialIntegrity.ts create mode 100644 packages/questionable-core/src/constructable/SurveyBuilderCore.ts create mode 100644 packages/questionable-core/src/constructable/_exports.ts create mode 100644 packages/questionable-core/src/constructable/lib/TQForm.ts create mode 100644 packages/questionable-core/src/constructable/lib/TSForm.ts create mode 100644 packages/questionable-core/src/constructable/lib/defaultReducer.ts create mode 100644 packages/questionable-core/src/constructable/lib/getBirthdate.ts create mode 100644 packages/questionable-core/src/constructable/lib/isSelected.ts create mode 100644 packages/questionable-core/src/constructable/lib/isValid.ts create mode 100644 packages/questionable-core/src/constructable/lib/pools.ts create mode 100644 packages/questionable-core/src/constructable/lib/toBirthdate.ts create mode 100644 packages/questionable-core/src/constructable/lib/toString.ts create mode 100644 packages/questionable-core/src/constructable/lib/updateForm.ts create mode 100644 packages/questionable-core/src/constructable/lib/validators.ts delete mode 100644 packages/questionable-core/src/lib/QuestionsCore.ts rename packages/questionable-core/src/{util => lib}/README.md (100%) delete mode 100644 packages/questionable-core/src/lib/StepsCore.ts rename packages/questionable-core/src/{util/index.ts => lib/_exports.ts} (53%) rename packages/questionable-core/src/{util => lib}/array.ts (77%) rename packages/questionable-core/src/{util => lib}/date.ts (68%) create mode 100644 packages/questionable-core/src/lib/enums.ts rename packages/questionable-core/src/{util => lib}/error.ts (88%) create mode 100644 packages/questionable-core/src/lib/factories.ts rename packages/questionable-core/src/{util => lib}/helpers.ts (100%) delete mode 100644 packages/questionable-core/src/lib/index.ts create mode 100644 packages/questionable-core/src/lib/instanceOf.ts create mode 100644 packages/questionable-core/src/lib/labels.ts create mode 100644 packages/questionable-core/src/lib/logger.ts create mode 100644 packages/questionable-core/src/lib/merge.ts rename packages/questionable-core/src/{util => lib}/noop.ts (51%) create mode 100644 packages/questionable-core/src/lib/set.ts create mode 100644 packages/questionable-core/src/lib/types.ts create mode 100644 packages/questionable-core/src/lib/uuid.ts rename packages/questionable-core/src/{survey => metadata}/IActionCore.ts (77%) create mode 100644 packages/questionable-core/src/metadata/IAnswerCore.ts create mode 100644 packages/questionable-core/src/metadata/IBranchCore.ts rename packages/questionable-core/src/{survey => metadata}/IButtonCore.ts (73%) create mode 100644 packages/questionable-core/src/metadata/IConfigCore.ts create mode 100644 packages/questionable-core/src/metadata/IEventCore.ts rename packages/questionable-core/src/{survey => metadata}/IFormCore.ts (77%) create mode 100644 packages/questionable-core/src/metadata/IPageCore.ts rename packages/questionable-core/src/{survey => metadata}/IPagesCore.ts (76%) create mode 100644 packages/questionable-core/src/metadata/IQuestionCore.ts create mode 100644 packages/questionable-core/src/metadata/IQuestionnaireCore.ts rename packages/questionable-core/src/{survey => metadata}/IRefCore.ts (61%) create mode 100644 packages/questionable-core/src/metadata/IRequirementCore.ts create mode 100644 packages/questionable-core/src/metadata/IResponseCore.ts rename packages/questionable-core/src/{survey => metadata}/IResultCore.ts (80%) create mode 100644 packages/questionable-core/src/metadata/ISectionCore.ts create mode 100644 packages/questionable-core/src/metadata/IStepCore.ts rename packages/questionable-core/src/{survey => metadata}/README.md (85%) rename packages/questionable-core/src/{survey/index.ts => metadata/_exports.ts} (50%) create mode 100644 packages/questionable-core/src/metadata/properties/MAction.ts create mode 100644 packages/questionable-core/src/metadata/properties/MAnswer.ts create mode 100644 packages/questionable-core/src/metadata/properties/MBase.ts create mode 100644 packages/questionable-core/src/metadata/properties/MBranch.ts create mode 100644 packages/questionable-core/src/metadata/properties/MCommon.ts create mode 100644 packages/questionable-core/src/metadata/properties/MConfig.ts create mode 100644 packages/questionable-core/src/metadata/properties/MEvent.ts create mode 100644 packages/questionable-core/src/metadata/properties/MForm.ts create mode 100644 packages/questionable-core/src/metadata/properties/MPage.ts create mode 100644 packages/questionable-core/src/metadata/properties/MPages.ts create mode 100644 packages/questionable-core/src/metadata/properties/MQuestion.ts create mode 100644 packages/questionable-core/src/metadata/properties/MQuestionnaire.ts create mode 100644 packages/questionable-core/src/metadata/properties/MRef.ts create mode 100644 packages/questionable-core/src/metadata/properties/MRequirement.ts create mode 100644 packages/questionable-core/src/metadata/properties/MResponse.ts create mode 100644 packages/questionable-core/src/metadata/properties/MResult.ts create mode 100644 packages/questionable-core/src/metadata/properties/MSection.ts create mode 100644 packages/questionable-core/src/metadata/properties/MStep.ts create mode 100644 packages/questionable-core/src/metadata/properties/README.md create mode 100644 packages/questionable-core/src/metadata/properties/_exports.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TActionType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TAnswerType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TBaseType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TBranchType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TButtonType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TPageType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TQuestionType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TRefType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TRequirementType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TResponseType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TResultType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TSectionType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/TStepType.ts create mode 100644 packages/questionable-core/src/metadata/properties/type/_exports.ts create mode 100644 packages/questionable-core/src/metadata/types/ClassProperties.ts create mode 100644 packages/questionable-core/src/metadata/types/TAgeCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TAnswerDataCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TCollectable.ts create mode 100644 packages/questionable-core/src/metadata/types/TContentCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TDesignType.ts create mode 100644 packages/questionable-core/src/metadata/types/TGateCore.ts create mode 100644 packages/questionable-core/src/metadata/types/THorizontalPositionCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TOpType.ts create mode 100644 packages/questionable-core/src/metadata/types/TPageDataCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TProgressBarStatusType.ts create mode 100644 packages/questionable-core/src/metadata/types/TReferentialble.ts create mode 100644 packages/questionable-core/src/metadata/types/TResultDataCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TStringDictionaryCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TTypeable.ts create mode 100644 packages/questionable-core/src/metadata/types/TVerticalPositionCore.ts create mode 100644 packages/questionable-core/src/metadata/types/TVisible.ts create mode 100644 packages/questionable-core/src/metadata/types/_exports.ts rename packages/questionable-core/src/schema/{index.ts => _exports.ts} (100%) create mode 100644 packages/questionable-core/src/state/_exports.ts delete mode 100644 packages/questionable-core/src/state/index.ts create mode 100644 packages/questionable-core/src/state/pubsub.ts delete mode 100644 packages/questionable-core/src/state/stepReducer.ts delete mode 100644 packages/questionable-core/src/survey/IBranchCore.ts delete mode 100644 packages/questionable-core/src/survey/IDesignDataCore.ts delete mode 100644 packages/questionable-core/src/survey/IEventCore.ts delete mode 100644 packages/questionable-core/src/survey/IPageDataCore.ts delete mode 100644 packages/questionable-core/src/survey/IQuestionDataCore.ts delete mode 100644 packages/questionable-core/src/survey/IQuestionableConfigCore.ts delete mode 100644 packages/questionable-core/src/survey/IQuestionnaireCore.ts delete mode 100644 packages/questionable-core/src/survey/IStepCore.ts delete mode 100644 packages/questionable-core/src/survey/IStepDataCore.ts create mode 100644 packages/questionable-core/src/tests/Scaffolding.ts delete mode 100644 packages/questionable-core/src/util/enums.ts delete mode 100644 packages/questionable-core/src/util/log.ts delete mode 100644 packages/questionable-core/src/util/types.ts delete mode 100644 packages/questionable-mocks/typings.d.ts delete mode 100644 packages/questionable-react-component/scripts/parseSchema.js create mode 100644 packages/questionable-react-component/src/components/lib/types.ts create mode 100644 packages/questionable-react-component/src/composable/Buttons.ts delete mode 100644 packages/questionable-react-component/src/composable/EventEmitter.ts delete mode 100644 packages/questionable-react-component/src/composable/Form.ts create mode 100644 packages/questionable-react-component/src/composable/Page.ts create mode 100644 packages/questionable-react-component/src/composable/PageData.ts create mode 100644 packages/questionable-react-component/src/composable/Question.ts create mode 100644 packages/questionable-react-component/src/composable/QuestionData.ts delete mode 100644 packages/questionable-react-component/src/composable/QuestionableConfig.ts create mode 100644 packages/questionable-react-component/src/composable/Step.ts create mode 100644 packages/questionable-react-component/src/composable/StepData.ts create mode 100644 packages/questionable-react-component/src/composable/Wizard.ts create mode 100644 packages/questionable-react-component/src/composable/config.ts delete mode 100644 packages/questionable-react-component/src/state/stepReducer.ts delete mode 100644 packages/questionable-react-component/src/survey/IAction.ts delete mode 100644 packages/questionable-react-component/src/survey/IBranch.ts delete mode 100644 packages/questionable-react-component/src/survey/IButton.ts delete mode 100644 packages/questionable-react-component/src/survey/IDesignData.ts delete mode 100644 packages/questionable-react-component/src/survey/IEvent.ts delete mode 100644 packages/questionable-react-component/src/survey/IForm.ts delete mode 100644 packages/questionable-react-component/src/survey/IPageData.ts delete mode 100644 packages/questionable-react-component/src/survey/IPages.ts delete mode 100644 packages/questionable-react-component/src/survey/IQuestionData.ts delete mode 100644 packages/questionable-react-component/src/survey/IQuestionable.ts delete mode 100644 packages/questionable-react-component/src/survey/IQuestionableConfig.ts delete mode 100644 packages/questionable-react-component/src/survey/IQuestionnaire.ts delete mode 100644 packages/questionable-react-component/src/survey/IRef.ts delete mode 100644 packages/questionable-react-component/src/survey/IResult.ts delete mode 100644 packages/questionable-react-component/src/survey/IStep.ts delete mode 100644 packages/questionable-react-component/src/survey/IStepData.ts delete mode 100644 packages/questionable-react-component/src/survey/README.md delete mode 100644 packages/questionable-react-component/src/survey/index.ts create mode 100644 packages/questionable-tests/src/App.tsx rename packages/ssa-eligibility/src/{index.ts => main.ts} (100%) delete mode 100644 types.d.ts diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 43c97e71..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/.pnp.cjs b/.pnp.cjs index 7eee928e..03a3f440 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -10252,7 +10252,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-yaml", "npm:0.5.0"],\ ["prettier", "npm:2.6.2"],\ ["prettier-plugin-organize-imports", "virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:2.3.4"],\ - ["pretty-quick", "virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:3.1.3"],\ ["rimraf", "npm:3.0.2"],\ ["rollup", "npm:2.70.2"],\ ["rollup-plugin-copy", "npm:3.4.0"],\ @@ -10397,7 +10396,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["short-unique-id", "npm:4.4.4"],\ ["ts-json-schema-generator", "npm:1.0.0"],\ ["ts-node", "virtual:c90bc748bea7f22dee4f33c258c50c0ffa66a59a97a976f3281c8a315370f6db880d0eec9bb57168b2ec5adc2c9a4d7c2bc5de6efdaf0208d4f3f6930f82324b#npm:10.7.0"],\ - ["tslib", "npm:2.4.0"],\ ["typescript", "patch:typescript@npm%3A4.6.3#~builtin::version=4.6.3&hash=bda367"],\ ["uuid", "npm:8.3.2"]\ ],\ @@ -10483,7 +10481,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/rimraf", "npm:3.0.2"],\ ["@typescript-eslint/eslint-plugin", "virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:5.21.0"],\ ["@typescript-eslint/parser", "virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:5.21.0"],\ - ["@usds.gov/questionable-build", "workspace:packages/questionable-build"],\ ["@usds.gov/questionable-core", "workspace:packages/questionable-core"],\ ["babel-loader", "virtual:9e4304a1ef9f2f27a1c168a696ea5f6c317ed4ef495adbb738e5cfc19f0ea6a8eceac7bd471718e6e4ae0912f24a63773a54430ca00a412e8431b883e3645d79#npm:8.2.5"],\ ["babel-preset-react-app", "npm:10.0.1"],\ @@ -10526,7 +10523,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["semantic-ui-react", "virtual:9e4304a1ef9f2f27a1c168a696ea5f6c317ed4ef495adbb738e5cfc19f0ea6a8eceac7bd471718e6e4ae0912f24a63773a54430ca00a412e8431b883e3645d79#npm:2.1.2"],\ ["ts-json-schema-generator", "npm:1.0.0"],\ ["ts-node", "virtual:c90bc748bea7f22dee4f33c258c50c0ffa66a59a97a976f3281c8a315370f6db880d0eec9bb57168b2ec5adc2c9a4d7c2bc5de6efdaf0208d4f3f6930f82324b#npm:10.7.0"],\ - ["tslib", "npm:2.4.0"],\ ["typescript", "patch:typescript@npm%3A4.6.3#~builtin::version=4.6.3&hash=bda367"],\ ["use-wizard", "npm:4.0.6"]\ ],\ @@ -10632,7 +10628,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-react-hooks", "virtual:9e4304a1ef9f2f27a1c168a696ea5f6c317ed4ef495adbb738e5cfc19f0ea6a8eceac7bd471718e6e4ae0912f24a63773a54430ca00a412e8431b883e3645d79#npm:4.5.0"],\ ["gh-pages", "npm:3.2.3"],\ ["lodash", "npm:4.17.21"],\ - ["pretty-quick", "virtual:3baf0c962b63f2dc858078b210c20c5f43c935ebbbd0cd377cdfbbc0e5e8e8cbfea283586f94aed8add36e9b34eae84560b772a19894292ba4e15121144fc8ca#npm:3.1.3"],\ ["react", "npm:17.0.2"],\ ["react-async", "virtual:3baf0c962b63f2dc858078b210c20c5f43c935ebbbd0cd377cdfbbc0e5e8e8cbfea283586f94aed8add36e9b34eae84560b772a19894292ba4e15121144fc8ca#npm:10.0.1"],\ ["react-docgen-typescript-loader", "virtual:9e4304a1ef9f2f27a1c168a696ea5f6c317ed4ef495adbb738e5cfc19f0ea6a8eceac7bd471718e6e4ae0912f24a63773a54430ca00a412e8431b883e3645d79#npm:3.7.2"],\ @@ -27830,44 +27825,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:3baf0c962b63f2dc858078b210c20c5f43c935ebbbd0cd377cdfbbc0e5e8e8cbfea283586f94aed8add36e9b34eae84560b772a19894292ba4e15121144fc8ca#npm:3.1.3", {\ - "packageLocation": "./.yarn/__virtual__/pretty-quick-virtual-a23fbe5a21/0/cache/pretty-quick-npm-3.1.3-b3fdb9cf9e-28bdc32571.zip/node_modules/pretty-quick/",\ - "packageDependencies": [\ - ["pretty-quick", "virtual:3baf0c962b63f2dc858078b210c20c5f43c935ebbbd0cd377cdfbbc0e5e8e8cbfea283586f94aed8add36e9b34eae84560b772a19894292ba4e15121144fc8ca#npm:3.1.3"],\ - ["@types/prettier", null],\ - ["chalk", "npm:3.0.0"],\ - ["execa", "npm:4.1.0"],\ - ["find-up", "npm:4.1.0"],\ - ["ignore", "npm:5.2.0"],\ - ["mri", "npm:1.2.0"],\ - ["multimatch", "npm:4.0.0"],\ - ["prettier", null]\ - ],\ - "packagePeers": [\ - "@types/prettier",\ - "prettier"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:3.1.3", {\ - "packageLocation": "./.yarn/__virtual__/pretty-quick-virtual-6e1511361e/0/cache/pretty-quick-npm-3.1.3-b3fdb9cf9e-28bdc32571.zip/node_modules/pretty-quick/",\ - "packageDependencies": [\ - ["pretty-quick", "virtual:4abdaac96bcf92ee1013007d151e2a65969ace133ced60c3aab4f2718e0f9f6cb571d7ac9de1597d1713ce3524fab4c6406035213bff7a25756a36c88c73a697#npm:3.1.3"],\ - ["@types/prettier", "npm:2.6.0"],\ - ["chalk", "npm:3.0.0"],\ - ["execa", "npm:4.1.0"],\ - ["find-up", "npm:4.1.0"],\ - ["ignore", "npm:5.2.0"],\ - ["mri", "npm:1.2.0"],\ - ["multimatch", "npm:4.0.0"],\ - ["prettier", "npm:2.6.2"]\ - ],\ - "packagePeers": [\ - "@types/prettier",\ - "prettier"\ - ],\ - "linkType": "HARD"\ - }],\ ["virtual:c90bc748bea7f22dee4f33c258c50c0ffa66a59a97a976f3281c8a315370f6db880d0eec9bb57168b2ec5adc2c9a4d7c2bc5de6efdaf0208d4f3f6930f82324b#npm:3.1.3", {\ "packageLocation": "./.yarn/__virtual__/pretty-quick-virtual-a495defcda/0/cache/pretty-quick-npm-3.1.3-b3fdb9cf9e-28bdc32571.zip/node_modules/pretty-quick/",\ "packageDependencies": [\ diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 7ab2d8eb..00000000 --- a/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -.cache -.yarn -package.json -public -yarn-lock.json \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml deleted file mode 100644 index f3bc425a..00000000 --- a/.prettierrc.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -printWidth: 80 -jsxBracketSameLine: false -singleQuote: true -tabWidth: 2 -trailingComma: 'all' -endOfLine: 'lf' diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 258009c6..f11e7bbb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,8 @@ "visualstudioexptteam.vscodeintellicode", "github.vscode-codeql", "github.vscode-pull-request-github", - "donjayamanne.git-extension-pack" + "donjayamanne.git-extension-pack", + "arcanis.vscode-zipfs", + "esbenp.prettier-vscode" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 22f5b47a..109f4ed0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,7 +43,9 @@ "type": "node", "request": "launch", "args": [ - "${relativeFile}" + //"${relativeFile}" + "packages/questionable-console/src/s.ts", + "init" ], "runtimeArgs": [ "--nolazy", diff --git a/package.json b/package.json index a23e839c..33abbce4 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "ts-node": "^10.7.0" }, "engines": { - "node": "14.x - 18.x" + "node": "14.x - 16.x" }, "homepage": "https://usds.github.io/questionable", "keywords": [ diff --git a/packages/questionable-build/package.json b/packages/questionable-build/package.json index 8db599d2..c635d931 100644 --- a/packages/questionable-build/package.json +++ b/packages/questionable-build/package.json @@ -47,7 +47,6 @@ "eslint-plugin-yaml": "^0.5.0", "prettier": "^2.6.2", "prettier-plugin-organize-imports": "^2.3.4", - "pretty-quick": "^3.1.3", "rimraf": "^3.0.2", "rollup": "^2.70.2", "rollup-plugin-copy": "^3.4.0", diff --git a/packages/questionable-build/packages_still_in_dist.txt b/packages/questionable-build/packages_still_in_dist.txt new file mode 100644 index 00000000..3e8e6ab9 --- /dev/null +++ b/packages/questionable-build/packages_still_in_dist.txt @@ -0,0 +1,61 @@ +"ts-json-schema-generator": "^1.0.0", +"semantic-ui-css": "^2.4.1", +"storybook-readme": "^5.0.9", +"react-docgen-typescript-loader": "^3.7.2", +"gh-pages": "^3.2.3", +"babel-loader": "^8.2.4", +"babel-preset-react-app": "^10.0.1", +"@types/react": "^17.0.43", +"@types/react-dom": "^17.0.14", +"@types/react-jsonschema-form": "^1.7.8", +"@types/file-saver": "^2.0.5", +"@types/gtag.js": "^0.0.10", +"@types/jest": "^27.4.1", +"@types/lodash": "^4.14.180", +"@types/luxon": "^2.3.1", +"@testing-library/jest-dom": "^5.16.3", +"@testing-library/react": "^12.1.4", +"@testing-library/user-event": "^13.5.0", +"@types/jest": "^27.4.1", +"@storybook/addon-actions": "^6.4.19", +"@storybook/addon-docs": "^6.4.19", +"@storybook/addon-essentials": "^6.4.19", +"@storybook/addon-knobs": "^6.4.0", +"@storybook/addon-links": "^6.4.19", +"@storybook/node-logger": "^6.4.19", +"@storybook/react": "^6.4.19", +"@storybook/storybook-deployer": "^2.8.11", +"@fortawesome/fontawesome-free": "^6.1.1", +"@mdx-js/react": "^2.1.0", +"@rjsf/semantic-ui": "^4.1.1", +"@rollup/plugin-commonjs": "^21.0.3", +"@rollup/plugin-node-resolve": "^13.1.3", +"@babel/core": "^7.17.8", +"@babel/plugin-transform-runtime": "^7.17.0", +"@babel/preset-env": "^7.16.11", +"jest": "^27.5.1", +"rollup": "^2.70.1", +"rollup-plugin-copy": "^3.4.0", +"rollup-plugin-dts": "^4.2.0", +"rollup-plugin-peer-deps-external": "^2.2.4", +"rollup-plugin-terser": "^7.0.2", +"rollup-plugin-ts": "^2.0.5", +"ts-jest": "^27.1.4", +"eslint-plugin-jest": "^26.1.3", +"eslint-plugin-jsx-a11y": "^6.5.1", +"eslint-plugin-react": "^7.29.4", +"eslint-plugin-react-hooks": "^4.3.0", +"eslint-plugin-testing-library": "^5.1.0", +"lodash": "^4.17.21", +"luxon": "^2.3.1", +"react": "^17.0.2", +"react-async": "^10.0.1", +"react-dom": "^17.0.2", +"react-scripts": "5.0.0", +"@trussworks/react-uswds": "^2.8.0", +"bluebird": "^3.7.2", +"cross-env": "7.0.3", + +yarn add -D --cached @swc/core @swc/helpers @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-airbnb-base eslint-config-prettier eslint-plugin-align-assignments eslint-plugin-align-import eslint-plugin-flowtype eslint-plugin-import eslint-plugin-json-files eslint-plugin-prettier eslint-plugin-sonarjs eslint-plugin-sort-keys-fix eslint-plugin-typescript-sort-keys eslint-plugin-yaml prettier prettier-plugin-organize-imports rimraf ts-node rollup rollup-plugin-copy rollup-plugin-dts rollup-plugin-peer-deps-external rollup-plugin-terser rollup-plugin-ts ts-json-schema-generator @babel/core @babel/plugin-transform-runtime @babel/preset-env @rollup/plugin-commonjs @rollup/plugin-node-resolve @swc/core @swc/helpers + +@babel/core @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-env @babel/runtime diff --git a/packages/questionable-build/src/parseSchema.ts b/packages/questionable-build/src/parseSchema.ts index 5249e3c0..d922e015 100644 --- a/packages/questionable-build/src/parseSchema.ts +++ b/packages/questionable-build/src/parseSchema.ts @@ -1,19 +1,11 @@ import fs from 'fs'; -const { log, error } = console; - export const parseSchema = (input = './dist/survey.json', output = './src/schema/survey.ts') => { - try { - log({ input, output }); - const rawdata = fs.readFileSync(input, 'utf-8'); + const rawdata = fs.readFileSync(input); - const schema = `// This files is code generated. Do not edit. + const schema = `// This files is code generated. Do not edit. /* eslint-disable */ export const survey = ${rawdata};`; - fs.writeFileSync(output, schema); - } catch (e) { - error(e); - log('Failed to make survey'); - } + fs.writeFileSync(output, schema); }; diff --git a/packages/questionable-console/src/composable/Iterable.ts b/packages/questionable-console/src/composable/Iterable.ts index 4d798029..822780d3 100644 --- a/packages/questionable-console/src/composable/Iterable.ts +++ b/packages/questionable-console/src/composable/Iterable.ts @@ -1,90 +1,112 @@ import { + GateLogicCore, FormCore, - IQuestionDataCore, - IStepCore, - QuestionableConfigCore, - QuestionsCore, - QUESTION_TYPE, - stepReducer, - StepsCore, + Questioner, + error, + log, + white, + yellow, } from '@usds.gov/questionable-core'; import { - Answers, DistinctQuestion, prompt, + Answers, + DistinctQuestion, + prompt, } from 'inquirer'; -import BottomBar from 'inquirer/lib/ui/bottom-bar'; import PromptUI from 'inquirer/lib/ui/prompt'; +import { merge } from 'lodash'; import { Observable, Subject } from 'rxjs'; -import { IQuestion } from '../survey/IStep'; +import { getAnswer } from '../util/helper'; +import { TVal } from '../util/types'; +import { PromptFactory } from './PromptFactory'; +import { Question } from './Question'; import { Questionnaire } from './Questionnaire'; +import { Step } from './Step'; +import '../util/inquirer'; -type TVal = { answer: any, name: string }; -type TChoice = { answer: string, key: string, name: string } -type TAnswerType = { - type: 'number' | 'input' | 'password' | 'list' | 'expand' | - 'checkbox' | 'confirm' | 'editor' | 'rawlist', - values?: string[] | TChoice[] -}; +export class Iterable { + #current: Question; -export class Iterable { - protected current: IQuestion; + public get current() { + return this.#current; + } + + private set current(val: Question) { + this.#current = val; + } + + #started = false; + + public get started() { + return this.#started; + } - protected started = false; + private set started(val: boolean) { + this.#started = val; + } - protected observable = new Subject>(); + #observable = new Subject>(); - protected form = new FormCore(); + protected get observable() { + return this.#observable; + } - protected process: Observable; + private set observable(val) { + this.#observable = val; + } - protected config = new QuestionableConfigCore(); + #form: F; - protected prompt: Promise & { + protected get form(): F { + return this.#form; + } + + private set form(val) { + this.#form = val; + } + + #process: Observable; + + protected get process() { + return this.#process; + } + + #prompt: Promise & { ui: PromptUI; }; - protected questionnaire: Questionnaire; + protected get prompt() { + return this.#prompt; + } + + #questionnaire: Q; + + protected get questionnaire() { + return this.#questionnaire; + } + + protected get config() { + return this.#questionnaire.config; + } - protected bottomBar!: BottomBar; + #gateLogic: GateLogicCore; + + protected get gateLogic() { + return this.#gateLogic; + } constructor( - questionnaire: Questionnaire, - config: QuestionableConfigCore = new QuestionableConfigCore(), + questionnaire: Q, + form: F, ) { - this.questionnaire = questionnaire; - this.config = config; - this.prompt = prompt(this.observable); - this.process = this.prompt.ui.process; - [this.current] = this.questionnaire.questions; - this.makeProgressBar(); - } - - // eslint-disable-next-line class-methods-use-this - getType(q: IQuestion): TAnswerType { - let ret: TAnswerType = { type: 'confirm' }; - switch (q.type) { - case QUESTION_TYPE.MULTIPLE_CHOICE: - ret = { - type: 'list', - values: q.answers.map((e, i) => ({ - answer: e.type || '', - key: `${i}`, - name: e.title || '', - })), - }; - break; - case QUESTION_TYPE.DOB: - ret = { - type: 'input', - }; - break; - default: - ret = { type: 'confirm' }; - break; - } - return ret; + this.#questionnaire = questionnaire; + this.#form = form; + this.#prompt = prompt(this.observable); + this.#process = this.#prompt.ui.process; + [this.#current] = this.#questionnaire.questions; + this.#gateLogic = new GateLogicCore(questionnaire, form); } - makePage(step: IStepCore) { + async makePage(step: Step) { this.observable.next({ message: step.title, name: step.id, @@ -93,24 +115,30 @@ export class Iterable { }); } - makeQuestion(question: IQuestion) { - const { type, values } = this.getType(question); - this.observable.next({ - choices: values, - message: question.title, - name: question.id, + async makeQuestion(question: Question) { + const mould = PromptFactory(question); + const prmpt = merge(mould, { + askAnswered: true, + default: question.default, + loop: true, + message: question.title, + name: question.id, // suffix: question.subTitle, - type, - validate: (answer) => { - QuestionsCore.updateForm(answer, this.props(question), this.config); - return StepsCore.isNextEnabled(this.props(question)); - }, - }); + validate: async (a: TVal) => this.validate(a, question), + }) as DistinctQuestion; + this.observable.next(prmpt); } - makeStep(step: IStepCore) { - if (StepsCore.getStepType(step) === 'question') { - const nextQuestion = step as IQuestion; + async makeStep(step: Step) { + if (step.onDisplay) { + yellow(this.current); + await step.onDisplay({ + answer: this.current.answer, + name: this.current.title, + }, step); + } + if (this.gateLogic.getStepType(step) === 'question') { + const nextQuestion = step as Question; this.makeQuestion(nextQuestion); this.current = nextQuestion; } else { @@ -118,44 +146,40 @@ export class Iterable { } } - next(val: TVal): IQuestion | undefined { + async next(val: TVal): Promise { try { if (!this.current) { - this.current = this.questionnaire.getFirstStep(); + this.current = this.gateLogic.getFirstStep(); return this.next({ answer: val.answer, name: this.current.id }); } if (val.name) { - this.current = this.questionnaire.getStepById(val.name) as IQuestion; + this.current = this.gateLogic.getStepById(val.name) as Question; } - if (this.questionnaire.isComplete(this.current.id)) { + const progress = this.gateLogic.getProgressPercent(this.current); + if (progress === 100 || this.gateLogic.isComplete(this.current)) { this.observable.complete(); return undefined; } - if (this.validate(val.answer, this.current)) { - this.current.exec(this.props(this.current)); - const nextStepId = this.questionnaire.getNextStep(this.props(this.current)); - const nextStep = this.questionnaire.getStepById(nextStepId); - this.makeStep(nextStep); + if (await this.validate(val, this.current)) { + if (this.current.onAnswer) { + await this.current.onAnswer(val, this.current); + } + const nextStep = this.gateLogic.getNextStep(this.current); + // const nextStep = this.gateLogic.getStepById(nextStepId); + await this.makeStep(nextStep); } else { - this.makeStep(this.current); + await this.makeStep(this.current); } + // log(`${this.gateLogic.getProgressPercent(this.current)}%`); return this.current; } catch (e) { - this.bottomBar.log.write(e); + error(e); } return undefined; } - props(question: IQuestion = this.current): IQuestionDataCore { - return ({ - dispatchForm: stepReducer, - form: this.form, - step: question, - stepId: question.id, - }); - } - - start() { + async start() { + log(this); if (this.started) return; this.process.subscribe({ @@ -165,40 +189,25 @@ export class Iterable { }); this.process.subscribe({ complete: () => { - console.log('log'); + white('complete'); }, error: (e) => { - console.error(e); + error(e); }, }); - this.makeQuestion(this.current); + await this.makeQuestion(this.current); this.started = true; } - validate(answer: string, question: IQuestion) { - QuestionsCore.updateForm(answer, this.props(question), this.config); - return StepsCore.isNextEnabled(this.props(question)); - } - - private makeProgressBar(spinnerText = 'Working') { - const loader = [ - `/ ${spinnerText}`, - `| ${spinnerText}`, - `\\ ${spinnerText}`, - `- ${spinnerText}`, - ]; - let i = 0; - this.bottomBar = new BottomBar(); - // this.bottomBar = new BottomBar({ bottomBar: `\r\n${loader[i]}` }); - - setInterval(() => { - // this.bottomBar.updateBottomBar(''); - // this.bottomBar.updateBottomBar(`\r\n${loader[i]}`); - if (i >= 3) { - i = 0; - } else { - i += 1; - } - }, 300); + async validate(a: TVal, question: Question) { + let isValid = true; + const answer = getAnswer(a); + if (question.validate) { + isValid = await question.validate(a, question); + } + const qrr = new Questioner({ form: this.form, question }); + qrr.updateForm({ answer }); + isValid = isValid && this.gateLogic.isNextEnabled(question); + return isValid; } } diff --git a/packages/questionable-console/src/composable/PromptFactory.ts b/packages/questionable-console/src/composable/PromptFactory.ts new file mode 100644 index 00000000..f2ce362a --- /dev/null +++ b/packages/questionable-console/src/composable/PromptFactory.ts @@ -0,0 +1,71 @@ +import { ANSWER_TYPE, QUESTION_TYPE } from '@usds.gov/questionable-core'; +import { Answers, DistinctQuestion } from 'inquirer'; +import { TAnswerMap } from '../util/types'; +import { Question } from './Question'; + +const ignorePaths = ['node_modules', '.']; + +export const PromptFactory = (q: Question): DistinctQuestion => { + let ret: TAnswerMap = { type: 'confirm' }; + if (q.componentType) { + switch (q.componentType) { + case 'path': + ret = { + depthLimit: 5, + excludeFilter: (nodePath) => nodePath === '.', + excludePath: (nodePath) => ignorePaths.some((p) => `${nodePath}`.startsWith(p)), + itemType: 'directory', + suggestOnly: false, + type: 'fuzzypath', + }; + break; + case 'date': + ret = { + type: 'date', + }; + break; + + default: + + break; + } + } else { + switch (q.type) { + case QUESTION_TYPE.MULTIPLE_CHOICE: + ret = { + choices: q.answers.map((e, i) => ({ + answer: e.type || ANSWER_TYPE.FIXED, + key: `${i}`, + name: e.title || '', + })), + type: 'list', + }; + break; + case QUESTION_TYPE.MULTIPLE_SELECT: + ret = { + choices: q.answers.map((e, i) => ({ + answer: `${e.type || ANSWER_TYPE.FIXED}`, + disabled: false, + key: `${i}`, + name: e.title || '', + })), + type: 'checkbox', + }; + break; + case QUESTION_TYPE.DOB: + ret = { + type: 'date', + }; + break; + case QUESTION_TYPE.TEXT: + ret = { + type: 'input', + }; + break; + default: + ret = { type: 'confirm' }; + break; + } + } + return ret as DistinctQuestion; +}; diff --git a/packages/questionable-console/src/composable/Question.ts b/packages/questionable-console/src/composable/Question.ts new file mode 100644 index 00000000..e6b63270 --- /dev/null +++ b/packages/questionable-console/src/composable/Question.ts @@ -0,0 +1,75 @@ +/* eslint-disable import/no-cycle */ +import { + noopAsync, + QuestionCore, + AnswerCore as Answer, + TQuestionType, +} from '@usds.gov/questionable-core'; +import { + TStringFn, +} from '../util/types'; +import { + TOnAnswer, + TOnDisplay, + TValidateFn, +} from './Step'; + +export class Question extends QuestionCore { + public static override create(data: Partial, order = 0) { + if (data instanceof Question) { + return data; + } + return new Question(data, order); + } + + #answers: Answer[]; + + #componentType?: 'date' | 'path' | undefined; + + #default?: string | TStringFn; + + #onAnswer?: TOnAnswer; + + #onDisplay?: TOnDisplay; + + #validate?: TValidateFn; + + constructor(data: Partial, order = 0) { + super(data); + this.#answers = data.answers?.map((a) => Answer.create(a)) || []; + this.set('order', order); + this.#componentType = data.componentType || undefined; + this.#default = data.default || ''; + this.#onAnswer = data.onAnswer || noopAsync; + this.#onDisplay = data.onDisplay || noopAsync; + this.#validate = data.validate || (async () => true); + } + + public get answers(): Answer[] { + return this.#answers; + } + + public get componentType() { + return this.#componentType; + } + + public get default() { + return this.#default; + } + + public get onAnswer() { + return this.#onAnswer; + } + + public get onDisplay() { + return this.#onDisplay; + } + + public get validate() { + return this.#validate; + } + + public get type(): TQuestionType { + return super.type; + } +} diff --git a/packages/questionable-console/src/composable/Questionnaire.ts b/packages/questionable-console/src/composable/Questionnaire.ts index f3d74e9d..7a92a700 100644 --- a/packages/questionable-console/src/composable/Questionnaire.ts +++ b/packages/questionable-console/src/composable/Questionnaire.ts @@ -1,20 +1,38 @@ +/* eslint-disable import/no-cycle */ import { + matches, QuestionnaireCore, } from '@usds.gov/questionable-core'; -import { IQuestionnaire } from '../survey/IQuestionnaire'; -import { IQuestion } from '../survey/IStep'; +import { Question } from './Question'; export class Questionnaire extends QuestionnaireCore { - questions: IQuestion[] = []; + #questions: Question[]; - constructor(data: Partial) { + constructor(data: Partial) { super(data); - if (data.questions) { - this.questions = data.questions; + this.#questions = data.questions?.map((q, i) => Question.create(q, i)) || []; + } + + public get questions(): Question[] { + return this.#questions; + } + + public existsIn(data: Question): boolean { + if (data instanceof Question) { + return this.#questions.some((q) => q === data || matches(q.title, data.title)); } + return super.existsIn(data); } - isComplete(stepId: string) { - return this.flow.indexOf(stepId) === this.flow.length - 1; + public add(data: Question): QuestionnaireCore { + const exists = this.existsIn(data); + if (!exists) { + if (data instanceof Question) { + this.#questions.push(data); + } else { + super.add(data); + } + } + return this; } } diff --git a/packages/questionable-console/src/composable/Step.ts b/packages/questionable-console/src/composable/Step.ts new file mode 100644 index 00000000..d5c6ee32 --- /dev/null +++ b/packages/questionable-console/src/composable/Step.ts @@ -0,0 +1,22 @@ +/* eslint-disable import/no-cycle */ +/* eslint-disable no-useless-constructor */ +import { + StepCore, +} from '@usds.gov/questionable-core'; +import { TVal } from '../util/types'; + +export type TOnAnswer = (answer: TVal, step: Step, ...params: unknown[]) => Promise; +export type TOnDisplay = (answer:TVal, step: Step, ...params: unknown[]) => Promise; +export type TValidateFn = (answer: TVal, step: Step, ...params: unknown[]) => Promise; + +export class Step extends StepCore { + constructor(data: Partial) { + super(data); + } + + onAnswer?: TOnAnswer | undefined; + + onDisplay?: TOnDisplay | undefined; + + validate?: TValidateFn | undefined; +} diff --git a/packages/questionable-console/src/examples/Scaffolding.ts b/packages/questionable-console/src/examples/Scaffolding.ts new file mode 100644 index 00000000..8f9168c0 --- /dev/null +++ b/packages/questionable-console/src/examples/Scaffolding.ts @@ -0,0 +1,29 @@ +import { + FormCore as Form, + SurveyBuilder, +} from '@usds.gov/questionable-core'; +import { Questionnaire } from '../composable/Questionnaire'; +import { Iterable } from '../composable/Iterable'; +import { build } from './scaffolding/onboarding'; + +export class Scaffolding { + questionnaire: Questionnaire; + + form: Form; + + iterable: Iterable; + + builder: SurveyBuilder; + + constructor() { + this.builder = new SurveyBuilder(); + this.questionnaire = this.init(); + this.form = new Form(); + this.iterable = new Iterable(this.questionnaire, this.form); + } + + init(): Questionnaire { + build(this.builder); + return this.builder.init(Questionnaire); + } +} diff --git a/packages/questionable-console/src/examples/index.ts b/packages/questionable-console/src/examples/index.ts new file mode 100644 index 00000000..e3c516a1 --- /dev/null +++ b/packages/questionable-console/src/examples/index.ts @@ -0,0 +1 @@ +export * from './Scaffolding'; diff --git a/packages/questionable-console/src/examples/scaffolding/onboarding.ts b/packages/questionable-console/src/examples/scaffolding/onboarding.ts new file mode 100644 index 00000000..48efee53 --- /dev/null +++ b/packages/questionable-console/src/examples/scaffolding/onboarding.ts @@ -0,0 +1,330 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable max-len */ +import { noop } from 'lodash'; +import os from 'os'; +import { + AnswerCore, + PagesCore, + ResultCore, + SectionCore, + SurveyBuilder, + PAGE_TYPE, + QUESTION_TYPE, + yellow, + SECTION_TYPE, + RESULT_TYPE, + TResultType, + blue, red, white, +} from '@usds.gov/questionable-core'; +import { TVal } from '../../util/types'; +import { Question } from '../../composable/Question'; + +export const build = (builder: SurveyBuilder) => { + const [onboarding] = builder.add(SectionCore, [ + { + requirements: [], + title: 'VA.gov Onboarding', + type: SECTION_TYPE.UNLOCKED, + }, + ]); + builder.setDefaults(onboarding); + + builder.add(ResultCore, [{ + label: 'Complete', + requirements: [], + title: 'Results', + type: RESULT_TYPE.MATCH as TResultType, + }, + ]); + builder.add(PagesCore, [{ + landing: { + body: 'Please answer the following questions to setup your environment.', + id: PAGE_TYPE.LANDING, + section: onboarding, + title: + 'Welcome to the scaffolding project. Press any key to continue...', + type: PAGE_TYPE.LANDING, + }, + noResults: { + id: PAGE_TYPE.NO_RESULTS, + section: onboarding, + title: 'No actions have been performed', + type: PAGE_TYPE.NO_RESULTS, + }, + results: { + id: PAGE_TYPE.RESULTS, + section: onboarding, + title: 'Success. Your project has been bootstrapped.', + type: PAGE_TYPE.RESULTS, + }, + summary: { + id: PAGE_TYPE.SUMMARY, + section: onboarding, + title: + 'Review the output and confirm that everything was successful.', + type: PAGE_TYPE.SUMMARY, + }, + }]); + const [YES, NO] = builder.add(AnswerCore, [ + { key: 'y', title: 'Yes' }, + { key: 'n', title: 'No' }, + ]); + const YES_NO = [YES, NO]; + + // const [respondYes] = builder.add(ResponseCore, [{ + // answers: [YES], + // question: A, + // }, + // ]); + // const [respondYesOrNo] = builder.add(ResponseCore, [{ + // answers: [YES, NO], + // question: A, + // }, + // ]); + // const [isFirstTime] = builder.add(RequirementCore, [{ + // responses: [respondYes], + // }, + // ]); + // const [hasAnsweredA] = builder.add(RequirementCore, [{ + // responses: [respondYesOrNo], + // }, + // ]); + + const repoChoices = builder.add(AnswerCore, [{ + key: 'a', + short: 'all', + title: 'All', + }, + { + key: 'f', + short: 'front', + title: 'Front end', + }, + { + key: 'b', + short: 'backend', + title: 'Back end', + }, + { + key: 'c', + short: 'choose', + title: 'Let me choose', + }, + { + key: 'n', + short: 'none', + title: 'None; I will choose later', + }, + ]); + const repositories = builder.add(AnswerCore, [ + 'content-build', + 'digitalservice', + 'va-tools', + 'va.gov-team', + 'vagov-content', + 'veteran-facing-services-tools', + 'vets-api', + 'vets-api-mockdata', + 'vets-json-schema', + 'vets-website', + ].map((a) => ({ title: a }))); + + // const [selectedChooseRepos] = builder.add(RequirementCore, [ + // { + // responses: builder.add(ResponseCore, [ + // { + // answers: [CHOOSE_REPOSITORIES], + // question: C, + // }, + // ]), + // }, + // ]); + + const [ + firstTime, + workingDirectory, + repoTypes, + manualRepoSelection, + ] = builder.add(Question, [{ + answers: YES_NO, + onAnswer: async () => noop(), + onDisplay: async () => { + blue( + 'This is the scaffolding project. You will be asked a series of questions which will guide you through the setup process.)', + ); + }, + title: + 'Is this your first time configuring this environment to run VA.gov?', + type: QUESTION_TYPE.MULTIPLE_CHOICE, + }, { + answers: YES_NO, + componentType: 'path', + default: os.homedir, + // entryRequirements: [isFirstTime], + onAnswer: async (a: TVal) => { + const path = a.value || a.answer || a.short; + white(`Working directory has been set to ${path}`); + }, + onDisplay: async () => { + blue( + "Welcome aboard. We'll have you up and running in no time. The first step is to choose where your working directory is located. You can select either the current directory or you can specify your own path (note: this can be changed later, but it may be very time consuming)", + ); + }, + section: onboarding, + title: 'What directory do you want to use for this project?', + type: QUESTION_TYPE.TEXT, + validate: async (a: TVal) => { + const path = a.value || a.answer || a.short; + const exists = true; // fs.existsSync(`${path}`); + if (!exists) { + red(`"${path}" isn't a valid path. Please enter another path.`); + } + return exists; + }, + }, { + answers: repoChoices, + entryRequirements: [], + id: 'C', + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async () => { + blue( + 'The next step is to pull down the source code for the projects you will need to work on. You can have everything, just the frontend, just the backed or decide for each repo.', + ); + }, + title: 'Which repositories do you need?', + type: QUESTION_TYPE.MULTIPLE_CHOICE, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: repositories, + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async () => { + blue('Please select which repos you would like to clone.'); + }, + title: 'Which repositories do you need?', + type: QUESTION_TYPE.MULTIPLE_SELECT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + if (Array.isArray(selected.answer)) { + for (const repo of selected.answer) { + yellow(`Cloning ${repo} from Github...`); + } + } + }, + title: 'Would you like to compile these projects now?', + type: QUESTION_TYPE.MULTIPLE_SELECT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'Your .git configuration does not have an email. Enter your email address:', + type: QUESTION_TYPE.TEXT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'Your .git configuration does not have a name. Enter your name:', + type: QUESTION_TYPE.TEXT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'You have selected to compile a backend project, but you do not have Ruby installed. Would you like to install Ruby?', + type: QUESTION_TYPE.TEXT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'You have selected to compile a backend project, but you do not have Ruby installed. Would you like to install Ruby?', + type: QUESTION_TYPE.MULTIPLE_CHOICE, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'You have selected to compile a frontend project, but your node version is incorrect. Would you like to install `nvm` and the correct version of NodeJs?', + type: QUESTION_TYPE.MULTIPLE_SELECT, + validate: async () => true, // fs.existsSync(`${path}`), + }, { + answers: [YES, NO], + // entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: TVal) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async (selected: TVal) => { + yellow(selected); + }, + title: 'Would you like to generate bookmarks for the README content?', + type: QUESTION_TYPE.MULTIPLE_SELECT, + validate: async () => true, // fs.existsSync(`${path}`), + }, + ]); + + // builder.add(ResultCore, [ + // { + // // action: { id: '0' }, + // id: '1', + // label: 'done', + // requirements: builder.add(RequirementCore, [ + // { + // explanation: 'Scaffolding complete', + + // // responses: [ + // // { + // // answers: [{ id: '1' }, { id: '0' }], + // // question: { id: 'A' }, + // // }, + // // ], + // }, + // ]), + // title: 'Completed Tasks', + // }, + // ]); + + return { + firstTime, + manualRepoSelection, + repoTypes, + workingDirectory, + }; +}; diff --git a/packages/questionable-console/src/index.ts b/packages/questionable-console/src/index.ts index c2097b67..6d994923 100644 --- a/packages/questionable-console/src/index.ts +++ b/packages/questionable-console/src/index.ts @@ -1,13 +1,8 @@ -import { - registerPrompt, -} from 'inquirer'; -import { simple_all } from '@usds.gov/questionable-mocks'; -import { Questionnaire } from './composable/Questionnaire'; -import { Iterable } from './composable/Iterable'; -import { IQuestionnaire } from './survey/IQuestionnaire'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -registerPrompt('date', require('inquirer-date-prompt')); +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { simple_all } from '@usds.gov/questionable-mocks'; +import { FormCore } from '@usds.gov/questionable-core'; +import { Questionnaire } from './composable/Questionnaire'; +import { Iterable } from './composable/Iterable'; export const Questionable = (): any => { // if (!questionnaire) { @@ -15,9 +10,10 @@ export const Questionable = (): any => { // } // const prompts = new Subject(); // const inq = prompt(prompts); - const data = simple_all as unknown as IQuestionnaire; + const data = simple_all as unknown as Partial; // eslint-disable-line camelcase + const form = new FormCore(); const questionnaire = new Questionnaire(data); - const iterator = new Iterable(questionnaire); + const iterator = new Iterable(questionnaire, form); iterator.start(); return iterator; }; diff --git a/packages/questionable-console/src/s.ts b/packages/questionable-console/src/s.ts new file mode 100644 index 00000000..87cc1563 --- /dev/null +++ b/packages/questionable-console/src/s.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable global-require */ +import { Command } from 'commander'; +import shell from 'shelljs'; +import { blue } from '@usds.gov/questionable-core'; +import { Scaffolding } from './examples/Scaffolding'; + +const program = new Command(); + +program + .name('va.gov-onboarding') + .description('CLI to onboard') + .version('0.1.0'); + +program.command('version') + .description('Gets the version') + .action(() => { + blue(`${shell.cat('package.json').grep('version')}`); + }); + +program + .command('init') + .description('Start the scaffolding wizard') + .action(async () => { + const run = new Scaffolding(); + run.iterable.start(); + }); + +program.parse(); diff --git a/packages/questionable-console/src/survey/IQuestionnaire.ts b/packages/questionable-console/src/survey/IQuestionnaire.ts deleted file mode 100644 index 0288dc5f..00000000 --- a/packages/questionable-console/src/survey/IQuestionnaire.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IQuestionnaireCore } from '@usds.gov/questionable-core'; -import { IQuestion, IStep } from './IStep'; - -export interface IQuestionnaire extends IQuestionnaireCore { - questions: IQuestion[], - steps: IStep[], -} diff --git a/packages/questionable-console/src/survey/IStep.ts b/packages/questionable-console/src/survey/IStep.ts deleted file mode 100644 index ed6a250e..00000000 --- a/packages/questionable-console/src/survey/IStep.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - IPageCore, - IQuestionCore, - IStepCore, - PAGE_TYPE, - QUESTION_TYPE, -} from '@usds.gov/questionable-core'; - -type TExec = (...params: unknown[]) => void; - -export interface IStep extends IStepCore { - exec?: TExec; -} - -export interface IQuestion extends IStep, IQuestionCore { - exec: TExec; - type: QUESTION_TYPE; -} - -export interface IPage extends IStep, IPageCore { - exec: TExec; - type: PAGE_TYPE; -} diff --git a/packages/questionable-console/src/util/helper.ts b/packages/questionable-console/src/util/helper.ts new file mode 100644 index 00000000..f8142021 --- /dev/null +++ b/packages/questionable-console/src/util/helper.ts @@ -0,0 +1,36 @@ +import { error } from '@usds.gov/questionable-core'; +import { TTypeVal } from './types'; + +// eslint-disable-next-line no-promise-executor-return +export const sleep = (delay: 1000) => new Promise((f) => setTimeout(f, delay)); + +function isString(val: unknown) { + return (val instanceof String || typeof val === 'string'); +} + +export function getAnswer(a?: TTypeVal): string { + if (!a) { + return ''; + } + if (Array.isArray(a)) { + return a.join(','); + } + if (Array.isArray(a.answer)) { + return a.answer.join(','); + } + if (isString(a)) { + return `${a}`; + } + if (isString(a.answer)) { + return `${a.answer}`; + } + if (isString(a.value)) { + return `${a.value}`; + } + if (isString(a.short)) { + return `${a.short}`; + } + const str = JSON.stringify(a); + error(`Could not determine value of answer ${str}`); + return str; +} diff --git a/packages/questionable-console/src/util/inquirer.ts b/packages/questionable-console/src/util/inquirer.ts new file mode 100644 index 00000000..95e18d9c --- /dev/null +++ b/packages/questionable-console/src/util/inquirer.ts @@ -0,0 +1,15 @@ +/* eslint-disable global-require, @typescript-eslint/no-var-requires */ +import { error } from '@usds.gov/questionable-core'; +import { registerPrompt } from 'inquirer'; +// import idp from 'inquirer-date-prompt'; + +try { + // registerPrompt('date', idp as any); +} catch (e) { + error(e); +} +try { + registerPrompt('fuzzypath', require('inquirer-fuzzy-path')); +} catch (e) { + error(e); +} diff --git a/packages/questionable-console/src/util/types.ts b/packages/questionable-console/src/util/types.ts new file mode 100644 index 00000000..2f4b42e7 --- /dev/null +++ b/packages/questionable-console/src/util/types.ts @@ -0,0 +1,35 @@ +// eslint-disable-next-line import/no-cycle +export type TVal = { + answer: string | number | boolean | string[], + name: string, + short?: string, + value?: string, +}; +export type TTypeVal = TVal | string[]; +export type TChoice = { answer: string, key: string, name: string } +// export type TChoices = string[] | TChoice[]; +export type TAnswerMap = { + choices?: string[] | TChoice[] | TChoicesFn; + clearable?: boolean, + default?: string, + depthLimit?: 1 | 2 | 3 | 4 | 5 | 6, + excludeFilter?: TBoolFn, + excludePath?: TBoolFn, + filter?: TNumberFn, + format?: Intl.DateTimeFormat, + itemType?: 'any' | 'directory' | 'file', + locale?: 'en-us', + message?: string, + name?: string, + rootPath?: string, + suggestOnly?: boolean, + transformer?: TStringFn, + type: 'number' | 'input' | 'password' | 'list' | 'expand' | + 'checkbox' | 'confirm' | 'editor' | 'rawlist' | 'fuzzypath' | 'date', + validate?: TBoolFn, + values?: string[] | TChoice[], +}; +export type TStringFn = (...params: unknown[]) => string; +export type TBoolFn = (...params: unknown[]) => boolean; +export type TNumberFn = (...params: unknown[]) => number; +export type TChoicesFn = (...params: unknown[]) => string[] | TChoice[]; diff --git a/packages/questionable-core/package.json b/packages/questionable-core/package.json index 55aabf4b..bf010201 100644 --- a/packages/questionable-core/package.json +++ b/packages/questionable-core/package.json @@ -9,12 +9,11 @@ "redux": "^4.2.0", "serialize-error": "9.1.0", "short-unique-id": "^4.4.4", - "tslib": "^2.4.0", "typescript": "^4.6.3", "uuid": "^8.3.2" }, "engines": { - "node": "14.x - 18.x" + "node": "14.x - 16.x" }, "devDependencies": { "@babel/core": "7.17.9", diff --git a/packages/questionable-core/src/composable/ActionCore.ts b/packages/questionable-core/src/composable/ActionCore.ts new file mode 100644 index 00000000..2e4bfa84 --- /dev/null +++ b/packages/questionable-core/src/composable/ActionCore.ts @@ -0,0 +1,97 @@ +/* eslint-disable import/no-cycle */ +import { IActionCore } from '../metadata/IActionCore'; +import { ACTION_TYPE, TActionType } from '../metadata/properties/type/TActionType'; +import { ButtonCore } from './ButtonCore'; +import { + checkInstanceOf, ClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { RefCore } from './RefCore'; +// import { classCreate } from '../constructable/Factory'; + +export class ActionCore extends RefCore implements IActionCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.action; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.action, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof ActionCore) { + return data; + } + return new ActionCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return ActionCore.create(data); + } + + #buttons: ButtonCore[] = []; + + #icon: string; + + #label: string; + + #subTitle: string; + + #type: TActionType; + + constructor(data: Partial) { + super(data); + // this.#buttons = data.buttons?.map((itm) => classCreate(EClassList.BUTTON, itm)) || []; + this.#label = data.label || ''; + this.#subTitle = data.subTitle || ''; + this.#type = data.type || ACTION_TYPE.NONE; + this.#icon = data.icon || ''; + } + + /** + * Buttons to complete the action + * @title Buttons + * @hidden + */ + public get buttons(): ButtonCore[] { + return this.#buttons; + } + + public set buttons(val: ButtonCore[]) { + this.#buttons = val; + } + + public get icon(): string { + return this.#icon; + } + + /** + * @title Description + */ + public get subTitle(): string { + return this.#subTitle; + } + + public set subTitle(val: string) { + this.#subTitle = val; + } + + public get label(): string { + return this.#label; + } + + /** + * @title Type + * @hidden + */ + public get type(): TActionType { + return this.#type; + } + + public set type(val: TActionType) { + this.#type = val; + } +} diff --git a/packages/questionable-core/src/composable/AnswerCore.ts b/packages/questionable-core/src/composable/AnswerCore.ts new file mode 100644 index 00000000..56cf0474 --- /dev/null +++ b/packages/questionable-core/src/composable/AnswerCore.ts @@ -0,0 +1,86 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { IAnswerCore } from '../metadata/IAnswerCore'; +import { ANSWER_TYPE, TAnswerType } from '../metadata/properties/type/TAnswerType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { QuestionCore } from './QuestionCore'; +import { RefCore } from './RefCore'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { classCreate } from '../constructable/Factory'; + +type TMatches = { + questions?: TCollectable[]; +} | { + answer?: TCollectable; +} | { + answers?: TCollectable[]; +}; +type TActionable = TCollectable & TMatches; + +export class AnswerCore extends RefCore implements IAnswerCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.answer; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.answer, ClassList.ref], obj }); + } + + public static override create(data: Partial): AnswerCore { + if (data instanceof AnswerCore) { + return data; + } + return new AnswerCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return AnswerCore.create(data); + } + + #key = ''; + + #questions: QuestionCore[] = []; + + #synonyms: string[] = []; + + #type: TAnswerType; + + constructor(data: Partial) { + super(data); + this.#key = data.key || ''; + this.#synonyms = data.synonyms || []; + this.#type = data.type || ANSWER_TYPE.FIXED; + this.#questions = data.questions?.map((itm) => classCreate(EClassList.QUESTION, itm)) || []; + } + + public get key() { + return this.#key; + } + + public get questions() { + return this.#questions; + } + + public get synonyms() { + return this.#synonyms; + } + + public get type(): TAnswerType { + return this.#type; + } + + public existsIn(data: TActionable): boolean { + return existsInPool(data, this); + } + + public add(data: TActionable): AnswerCore { + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/BaseCore.ts b/packages/questionable-core/src/composable/BaseCore.ts new file mode 100644 index 00000000..ac16d516 --- /dev/null +++ b/packages/questionable-core/src/composable/BaseCore.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/no-cycle */ +import { cloneDeep, merge, noop } from 'lodash'; +import { TInstanceOf, checkInstanceOf, ClassList } from '../lib/instanceOf'; + +export interface TBaseSource { + [key: string]: unknown; +} +/** + * Generic class from which all others are derived + */ +export abstract class BaseCore implements TBaseSource { + [key: string]: unknown; + + /** + * Stash a copy of the original object for future inspection, + * primarily to aid debugging when classes are instantiated with + * undeclared properties + */ + #source: TBaseSource = {}; + + /** + * NOTE: we don't want this accidentally serializing; hence, + * `getSource()` and not `get source()` + * @returns Deep clone of the object used to instantiate this instance + */ + getSource(): TBaseSource { + return cloneDeep(this.#source); + } + + /** + * Instance comparator + */ + public abstract get instanceOfCheck(): TInstanceOf; + + /** + * Implement our own compator for eval using `instanceof` + * @param obj any object to compare at runtime + * @returns + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.base, ClassList.ref], obj }); + } + + /** + * Unambiguously cast or construct an object to target type + * @param data any object (or lack thereof) to be evaled as a ref + * @returns + */ + public static create(data: unknown): BaseCore { + if (data instanceof BaseCore) { + return data; + } + return data as BaseCore; + } + + /** + * For use when the property is not required + */ + public static createOptional(data?: unknown): BaseCore | undefined { + if (!data) { + return undefined; + } + return BaseCore.create(data); + } + + /** + * At some point, we might care to know what undeclared properties OR + * original values for declared properties have been passed in; therefore, + * these will be stashed by value internally + * @param data original object + */ + constructor(data: unknown) { + merge(this.#source, cloneDeep(data)); + } + + // public abstract existsIn(val: TBaseSource): boolean; + + /** KLUDGE: + * allow interface/abstract/base classes to implement a property so that + * the code will compile (fixes "class member does not use `this`" and + * "unused parameter" errors), for use when the primary purpose + * of the property is to setup inheritance + */ + protected noop = noop; + + /** + * This does nothing but establish the pattern for all other classes to build from; + * @param data any questionable object + */ + // public abstract add(data: TBaseSource): TBaseSource; +} diff --git a/packages/questionable-core/src/composable/BranchCore.ts b/packages/questionable-core/src/composable/BranchCore.ts new file mode 100644 index 00000000..32a9ff8b --- /dev/null +++ b/packages/questionable-core/src/composable/BranchCore.ts @@ -0,0 +1,76 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { IBranchCore } from '../metadata/IBranchCore'; +import { BRANCH_TYPE, TBranchType } from '../metadata/properties/type/TBranchType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { QuestionCore } from './QuestionCore'; +import { RefCore } from './RefCore'; +import { SectionCore } from './SectionCore'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { classCreate } from '../constructable/Factory'; + +type TBranchable = TCollectable & { + branch?: BranchCore; +} + +export class BranchCore extends RefCore implements IBranchCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.branch; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.branch, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof BranchCore) { + return data; + } + return new BranchCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return BranchCore.create(data); + } + + #questions; + + #sections; + + #type: TBranchType; + + constructor(data: Partial) { + super(data); + this.#questions = data.questions?.map((itm) => classCreate(EClassList.QUESTION, itm)) || []; + this.#sections = data.sections?.map((itm) => classCreate(EClassList.SECTION, itm)) || []; + this.#type = data.type || BRANCH_TYPE.LINEAR; + } + + public get questions(): QuestionCore[] { + return this.#questions; + } + + public get sections(): SectionCore[] { + return this.#sections; + } + + public get type(): TBranchType { + return this.#type; + } + + public existsIn(data: TBranchable): boolean { + return existsInPool(data, this); + } + + public add(data: TBranchable): BranchCore { + data.branch = this; // eslint-disable-line no-param-reassign + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/ButtonCore.ts b/packages/questionable-core/src/composable/ButtonCore.ts new file mode 100644 index 00000000..918285b1 --- /dev/null +++ b/packages/questionable-core/src/composable/ButtonCore.ts @@ -0,0 +1,80 @@ +/* eslint-disable import/no-cycle */ +import { IButtonCore } from '../metadata/IButtonCore'; +import { BUTTON_TYPE, TButtonType } from '../metadata/properties/type/TButtonType'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { RefCore } from './RefCore'; + +export class ButtonCore extends RefCore implements IButtonCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.button; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.button, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof ButtonCore) { + return data; + } + return new ButtonCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return ButtonCore.create(data); + } + + /** + * Link to tie to button click + * + * @title Link + */ + #link?: string | undefined; + + #type: TButtonType; + + /** + * Render mode (link or button) + * + * @title Mode + */ + get type(): TButtonType { + return this.#type; + } + + /** + * Visibility status of the button (show/hide) + * + * @title Visible + */ + #visible?: boolean | undefined; + + constructor(data: Partial) { + super(data); + this.#link = data.link || ''; + this.#visible = data.visible !== false; + this.#type = data.type || BUTTON_TYPE.BUTTON; + } + + public get link(): string { + return this.#link || ''; + } + + public set link(val: string) { + this.#link = val; + } + + public get visible(): boolean { + return this.#visible !== false; + } + + public set visible(val: boolean) { + this.#visible = val; + } + + public pointer?: 'back' | 'next' | undefined; +} diff --git a/packages/questionable-core/src/composable/ConfigCore.ts b/packages/questionable-core/src/composable/ConfigCore.ts new file mode 100644 index 00000000..d95bb653 --- /dev/null +++ b/packages/questionable-core/src/composable/ConfigCore.ts @@ -0,0 +1,193 @@ +/* eslint-disable import/no-cycle */ +import { + isEmpty, isString, merge, noop, +} from 'lodash'; +import { EventEmitterCore } from './EventEmitterCore'; +import { isEnum, MODE } from '../lib/enums'; +import { TGetDictionaryCore, TStringDictionaryCore } from '../metadata/types/TStringDictionaryCore'; +import { + NavigationConfigCore, + PagesConfigCore, + ProgressBarConfigCore, + QuestionConfigCore, + StepConfigCore, +} from './config/_exports'; +import { IQuestionableConfigCore } from '../metadata/IConfigCore'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { BaseCore } from './BaseCore'; + +const defaults = { + events: { + onActionClick: noop, + onAnswer: noop, + onAnyEvent: noop, + onBranch: noop, + onError: noop, + onGateSwitch: noop, + onInit: noop, + onNoResults: noop, + onPage: noop, + onResults: noop, + }, + mode: MODE.VIEW, + nav: {}, + pages: {}, + params: { dev: false }, + progressBar: {}, + questions: {}, +}; + +/** + * Configuration class for customizing the Questionable components + * + * The config has opinionated defaults, but is easily modified using Partial updates + */ +export class QuestionableConfigCore + extends BaseCore + implements IQuestionableConfigCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial) { + if (data instanceof QuestionableConfigCore) { + return data; + } + return new QuestionableConfigCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return QuestionableConfigCore.create(data); + } + + #mode!: MODE; + + #nav!: NavigationConfigCore; + + #pages!: PagesConfigCore; + + #progressBar!: ProgressBarConfigCore; + + #questions!: QuestionConfigCore; + + #steps!: StepConfigCore; + + #events!: EventEmitterCore; + + #params!: TStringDictionaryCore; + + readonly getRuntimeConfig?: TGetDictionaryCore; + + constructor(data: Partial = {}) { + super(data); + merge(this, defaults, data); + this.#params = data.getRuntimeConfig ? data.getRuntimeConfig(this) : {}; + if (data.params?.dev) { + this.#mode = MODE.DEV; + } + this.#events = EventEmitterCore.create(data.events); + this.#steps = StepConfigCore.create(data.steps); + this.#questions = QuestionConfigCore.create(data.questions); + this.#progressBar = ProgressBarConfigCore.create(data.progressBar); + this.#pages = PagesConfigCore.create(data.pages); + this.#nav = NavigationConfigCore.create(data.nav); + } + + get dev(): boolean { + return this.#mode === MODE.DEV; + } + + get events(): EventEmitterCore { + return this.#events; + } + + private set events(val: EventEmitterCore) { + this.#events = val; + } + + get mode(): MODE { + return this.#mode; + } + + set mode(val: MODE | string) { + if (isString(val)) { + if (isEnum({ enm: MODE, value: val })) { + this.#mode = val as MODE; + } else { + this.#mode = MODE.VIEW; + } + } else { + this.#mode = val; + } + } + + get params(): TStringDictionaryCore { + if (isEmpty(this.#params)) { + this.#params = {}; + } + return this.#params; + } + + get nav(): NavigationConfigCore { + if (isEmpty(this.#nav)) { + this.#nav = new NavigationConfigCore(); + } + return this.#nav; + } + + set nav(val: NavigationConfigCore) { + merge(this.#nav, val); + } + + get pages(): PagesConfigCore { + if (isEmpty(this.#pages)) { + this.#pages = new PagesConfigCore(); + } + return this.#pages; + } + + set pages(val: PagesConfigCore) { + merge(this.#pages, val); + } + + get progressBar(): ProgressBarConfigCore { + if (isEmpty(this.#progressBar)) { + this.#progressBar = new ProgressBarConfigCore(); + } + return this.#progressBar; + } + + set progressBar(val: ProgressBarConfigCore) { + merge(this.#progressBar, val); + } + + get questions(): QuestionConfigCore { + if (isEmpty(this.#questions)) { + this.#questions = new QuestionConfigCore(); + } + return this.#questions; + } + + set questions(val: QuestionConfigCore) { + merge(this.#questions, val); + } + + get steps(): StepConfigCore { + if (isEmpty(this.#steps)) { + this.#steps = new StepConfigCore(); + } + return this.#steps; + } + + set steps(val: StepConfigCore) { + merge(this.#steps, val); + } +} diff --git a/packages/questionable-core/src/composable/Dictionary.ts b/packages/questionable-core/src/composable/Dictionary.ts new file mode 100644 index 00000000..ab1ded33 --- /dev/null +++ b/packages/questionable-core/src/composable/Dictionary.ts @@ -0,0 +1,49 @@ +import { isEmpty } from 'lodash'; +import { ClassList, TInstanceOf } from '../lib/instanceOf'; +import { BaseCore } from './BaseCore'; + +export class Dictionary extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.base; + } + + #hash: Map; + + #init: unknown; + + constructor() { + super({}); + this.#hash = new Map(); + } + + public init(data: I): I { + if (!this.#init) { + this.#init = data; + } + return this.#init as I; + } + + public touch(name: K, val: V): T { + if (!this.isSet(name)) { + this.#hash.set(name, val); + } + return this.#hash.get(name) as T; + } + + public set(name: K, val: V, readonly = false) { + if (readonly && this.isSet(name)) return; + this.#hash.set(name, val); + } + + public isSet(name: K): boolean { + if (!this.#hash.has(name)) { + return false; + } + const val: T = this.#hash.get(name) as T; + return isEmpty(val); + } + + public get(name: K): T { + return this.#hash.get(name) as T; + } +} diff --git a/packages/questionable-core/src/composable/EventEmitterCore.ts b/packages/questionable-core/src/composable/EventEmitterCore.ts index 56ae96ad..45eec241 100644 --- a/packages/questionable-core/src/composable/EventEmitterCore.ts +++ b/packages/questionable-core/src/composable/EventEmitterCore.ts @@ -1,43 +1,76 @@ -import { catchError } from '../util/error'; -import { error as log, noop } from '../util'; -import { IFormCore } from '../survey'; +/* eslint-disable import/no-cycle */ +import { noop } from 'lodash'; import { IEventCore, - TAnswerDataCore, +} from '../metadata/IEventCore'; +import { TEventCore, TGateDataCore, TOnErrorCore, TOnEventCore, - TPageDataCore, - TResultDataCore, -} from '../survey/IEventCore'; +} from '../metadata/types/TGateCore'; +import { TResultDataCore } from '../metadata/types/TResultDataCore'; +import { TAnswerDataCore } from '../metadata/types/TAnswerDataCore'; +import { TPageDataCore } from '../metadata/types/TPageDataCore'; +import { catchError } from '../lib/error'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { error as log } from '../lib/logger'; +import { BaseCore } from './BaseCore'; +import { FormCore } from './FormCore'; + +const className = ClassList['event-emitter']; +export class EventEmitterCore extends BaseCore implements IEventCore { + public get instanceOfCheck(): TInstanceOf { + return className; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [className], obj }); + } -export class EventEmitterCore implements IEventCore { - onActionClick: TOnEventCore = noop; + readonly onActionClick: TOnEventCore; - onAnswer: TOnEventCore = noop; + readonly onAnswer: TOnEventCore; - onBranch: TOnErrorCore = noop; + readonly onBranch: TOnEventCore; - onAnyEvent: TOnEventCore = noop; + readonly onAnyEvent: TOnEventCore; - onGateSwitch: TOnEventCore = noop; + readonly onGateSwitch: TOnEventCore; - onError: TOnErrorCore = noop; + readonly onError: TOnErrorCore; - onPage: TOnEventCore = noop; + readonly onPage: TOnEventCore; - onInit: TOnEventCore = noop; + readonly onInit: TOnEventCore; - onResults: TOnEventCore = noop; + readonly onResults: TOnEventCore; - onNoResults: TOnEventCore = noop; + readonly onNoResults: TOnEventCore; + + public static override create(data: Partial = {}) { + if (data instanceof EventEmitterCore) { + return data; + } + return new EventEmitterCore(data); + } - constructor(obj: Partial) { - Object.assign(this, obj); + constructor(data: Partial = {}) { + super(data); + this.onActionClick = data.onActionClick || noop; + this.onAnswer = data.onAnswer || noop; + this.onAnyEvent = data.onAnyEvent || noop; + this.onBranch = data.onBranch || noop; + this.onError = data.onError || noop; + this.onGateSwitch = data.onGateSwitch || noop; + this.onInit = data.onInit || noop; + this.onNoResults = data.onNoResults || noop; + this.onPage = data.onPage || noop; + this.onResults = data.onResults || noop; } - action(data: IFormCore): void { + action(data: FormCore): void { this.#event(data, this.onActionClick); } @@ -57,11 +90,11 @@ export class EventEmitterCore implements IEventCore { } } - init(data: IFormCore): void { + init(data: FormCore): void { this.#event(data, this.onInit); } - noResult(data: IFormCore): void { + noResult(data: FormCore): void { this.#event(data, this.onNoResults); } @@ -88,7 +121,7 @@ export class EventEmitterCore implements IEventCore { try { callback(data); } catch (e) { - const error = catchError(e); + const error = catchError({ e }); this.error(error, data); } } diff --git a/packages/questionable-core/src/composable/FormCore.ts b/packages/questionable-core/src/composable/FormCore.ts index 2260621e..39a434db 100644 --- a/packages/questionable-core/src/composable/FormCore.ts +++ b/packages/questionable-core/src/composable/FormCore.ts @@ -1,40 +1,138 @@ -import { IFormCore } from '../survey/IFormCore'; -import { IQuestionCore } from '../survey/IStepCore'; -import { QuestionnaireCore } from './QuestionnaireCore'; -import { TAgeCore } from '../util/types'; - -export interface IFormConstructorCore { - form?: Partial; - questionnaire?: QuestionnaireCore; -} +/* eslint-disable import/no-cycle */ +import { eventedCore } from '../state/pubsub'; +import { IFormCore } from '../metadata/IFormCore'; +import { matches } from '../lib/helpers'; +import { TAgeCore } from '../metadata/types/TAgeCore'; +import { BaseCore } from './BaseCore'; +import { + checkInstanceOf, + ClassList, + TInstanceOf, +} from '../lib/instanceOf'; +import { QuestionCore } from './QuestionCore'; +import { defaultReducer, TStepReducerAction, type TReducerFn } from '../constructable/lib/defaultReducer'; + +export interface FormCore extends BaseCore, IFormCore {} + +export class FormCore extends BaseCore implements IFormCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.form; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.form], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof FormCore) { + return data; + } + return new FormCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return FormCore.create(data); + } + + #started; + + #birthdate; + + #age; + + #finished; + + #responses; + + constructor(data: Partial = {}) { + super(data); + this.#started = new Date(); + this.#age = data.age; + this.#finished = data.finished; + // this.#responses = data.responses || []; + this.#birthdate = data.birthdate || ''; + this.#responses = data.responses?.map((r) => QuestionCore.create(r)) || []; + eventedCore.publish({ event: this, type: 'start' }); + } -export class FormCore implements IFormCore { - public readonly started: Date; + public get started(): Date { + return this.#started; + } - #finished?: Date; + public get birthdate(): string { + return this.#birthdate; + } - public birthdate?: string; + public set birthdate(val: string) { + this.#birthdate = val; + } - public age?: TAgeCore; + public get age(): TAgeCore | undefined { + return this.#age; + } - public get finish(): Date | undefined { + public set age(data: TAgeCore | undefined) { + this.#age = data; + } + + public get finished(): Date | undefined { return this.#finished; } - public set finish(date: Date | undefined) { - if (date) { + public set finished(date: Date | undefined) { + if (date && !this.#finished) { this.#finished = date; + eventedCore.publish({ event: this, type: 'finish' }); } } - public responses: IQuestionCore[] = []; + public get responses(): QuestionCore[] { + return this.#responses; + } + + public set responses(val: QuestionCore[]) { + this.#responses = val; + } - constructor(data: IFormConstructorCore = { form: {} }) { - const { form, questionnaire } = data; - Object.assign(this, form); - this.started = new Date(); - if (questionnaire?.config.events.onInit) { - questionnaire.config.events.onInit(this); + public existsIn(data: QuestionCore): boolean { + if (data instanceof QuestionCore) { + return this.#responses.some( + (q) => q === data || matches(q.title, data.title), + ); + } + return false; + } + + public add(data: QuestionCore): FormCore { + const exists = this.existsIn(data); + if (exists) { + return this; + } + if (data instanceof QuestionCore) { + this.#responses.push(data); } + return this; + } + + /** + * Merges the form's answer state as the user progresses through the survey + * @param previousState + * @param action + * @returns + */ + public static reducer({ form, action, callback }: + {action: TStepReducerAction; callback?: TReducerFn; form: Partial}) { + return defaultReducer({ + Form: FormCore, action, callback, oldState: form, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reduce({ action, callback }: { action: TStepReducerAction; callback?: TReducerFn }) { + return FormCore.reducer({ action, callback, form: this }); } } diff --git a/packages/questionable-core/src/composable/PageCore.ts b/packages/questionable-core/src/composable/PageCore.ts new file mode 100644 index 00000000..404ee0e1 --- /dev/null +++ b/packages/questionable-core/src/composable/PageCore.ts @@ -0,0 +1,83 @@ +/* eslint-disable import/no-cycle */ +import { TCollectable } from '../metadata/types/TCollectable'; +import { TPointerDirection } from '../lib/types'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { StepCore } from './StepCore'; +import { IPageCore } from '../metadata/IPageCore'; +import { PAGE_TYPE, TPageType } from '../metadata/properties/type/TPageType'; + +const className = ClassList.page; +export class PageCore extends StepCore implements IPageCore { + public override get instanceOfCheck(): TInstanceOf { + return className; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [className, ClassList.page], obj }); + } + + public static override create(data: Partial) { + if (data instanceof PageCore) { + return data; + } + return new PageCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return PageCore.create(data); + } + + #display = true; + + constructor(data: Partial) { + super(data); + + const type = (!data.type || `${data.type}` === `${PAGE_TYPE.DEFAULT}`) ? PAGE_TYPE.LANDING : data.type; + this.#type = type; + this.#body = data.body || ''; + this.#bodyHeader = data.bodyHeader || ''; + this.#bodySubHeader = data.bodySubHeader || ''; + this.#display = !(data.display === false); + } + + #body: string; + + public get body() { + return this.#body; + } + + #bodyHeader: string; + + public get bodyHeader() { + return this.#bodyHeader; + } + + #bodySubHeader: string; + + public get bodySubHeader() { + return this.#bodySubHeader; + } + + public get display() { + return this.#display; + } + + #type: TPageType; + + public override get type() { + return this.#type; + } + + public existsIn(data: TCollectable, direction?: TPointerDirection): boolean { + return super.existsIn(data, direction); + } + + public add(data: TCollectable, direction?: TPointerDirection): PageCore { + super.add(data, direction); + return this; + } +} diff --git a/packages/questionable-core/src/composable/PagesCore.ts b/packages/questionable-core/src/composable/PagesCore.ts new file mode 100644 index 00000000..54401ccb --- /dev/null +++ b/packages/questionable-core/src/composable/PagesCore.ts @@ -0,0 +1,152 @@ +/* eslint-disable import/no-cycle */ +import { PAGE_TYPE, TPageType } from '../metadata/properties/type/TPageType'; +import { IPagesCore } from '../metadata/IPagesCore'; +import { matches } from '../lib/helpers'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { merge } from '../lib/merge'; +import { BaseCore } from './BaseCore'; +import { PageCore } from './PageCore'; + +export class PagesCore extends BaseCore implements IPagesCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.pages; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.pages], obj }); + } + + #pages: PageCore[]; + + public static override create(data: Partial) { + if (data instanceof PagesCore) { + return data; + } + return new PagesCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data) { + return undefined; + } + return PagesCore.create(data); + } + + /** + * Produces required object from inputs + * @param type Page Type + * @param data Optional data + * @returns + */ + #touchPage(type: TPageType, data?: Partial): Partial { + const defaults = merge( + { + params: [{ + display: false, + title: type, + }, + data, + { id: type, type }], + }, + ); + defaults.type = type; + return defaults; + } + + constructor(data: Partial = {}) { + super(data); + this.#pages = data.pages?.map((p) => PageCore.create(p)) || []; + this.#landingPage = PageCore.create( + this.#touchPage(PAGE_TYPE.LANDING, data.landingPage), + ); + this.#resultsPage = PageCore.create( + this.#touchPage(PAGE_TYPE.RESULTS, data.resultsPage), + ); + this.#noResultsPage = PageCore.create( + this.#touchPage(PAGE_TYPE.NO_RESULTS, data.noResultsPage), + ); + this.#summaryPage = PageCore.create( + this.#touchPage(PAGE_TYPE.SUMMARY, data.summaryPage), + ); + } + + #landingPage: PageCore; + + public get landingPage(): PageCore { + return this.#landingPage; + } + + #noResultsPage: PageCore; + + public get noResultsPage(): PageCore { + return this.#noResultsPage; + } + + #resultsPage: PageCore; + + public get resultsPage(): PageCore { + return this.#resultsPage; + } + + #summaryPage: PageCore; + + public get summaryPage() { + return this.#summaryPage; + } + + public get pages() { + return this.#pages; + } + + public all() { + return [ + ...this.#pages, + this.#landingPage, + this.#noResultsPage, + this.#resultsPage, + this.#summaryPage, + ]; + } + + public set(data: Partial) { + const page = PageCore.create(data); + switch (page.type) { + case PAGE_TYPE.LANDING: + this.#landingPage = page; + break; + case PAGE_TYPE.NO_RESULTS: + this.#noResultsPage = page; + break; + case PAGE_TYPE.RESULTS: + this.#resultsPage = page; + break; + case PAGE_TYPE.SUMMARY: + this.#summaryPage = page; + break; + default: + this.add(page); + break; + } + return page; + } + + public existsIn(data: PageCore): boolean { + if (data instanceof PageCore) { + return Object.values(this.pages).some( + (q) => q === data || matches(q.title, data.title), + ); + } + return false; + } + + public add(data: PageCore): PagesCore { + if (data instanceof PageCore) { + const exists = this.existsIn(data); + if (!exists) { + this.pages.push(data); + } + } + return this; + } +} diff --git a/packages/questionable-core/src/composable/QuestionCore.ts b/packages/questionable-core/src/composable/QuestionCore.ts new file mode 100644 index 00000000..0bd47f23 --- /dev/null +++ b/packages/questionable-core/src/composable/QuestionCore.ts @@ -0,0 +1,118 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { IQuestionCore } from '../metadata/IQuestionCore'; +import { QUESTION_TYPE, TQuestionType } from '../metadata/properties/type/TQuestionType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { TPointerDirection } from '../lib/types'; +import { AnswerCore } from './AnswerCore'; +import { BranchCore } from './BranchCore'; +import { SectionCore } from './SectionCore'; +import { StepCore } from './StepCore'; +import { classCreate } from '../constructable/Factory'; + +export class QuestionCore extends StepCore implements IQuestionCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.question; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.question, ClassList.step], obj }); + } + + public static override create(data: Partial) { + if (data instanceof QuestionCore) { + return data; + } + return new QuestionCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return QuestionCore.create(data); + } + + // #type: QUESTION_TYPE; + + #answers: AnswerCore[]; + + #branch: BranchCore | undefined; + + #section: SectionCore | undefined; + + #answer = ''; + + #answered: string[] = []; + + #type: TQuestionType; + + constructor(data: Partial) { + super(data); + const type: TQuestionType = (!data.type || `${data.type}` === `${QUESTION_TYPE.DEFAULT}`) + ? QUESTION_TYPE.TEXT : data.type; + this.#type = type; + this.#answers = data.answers?.map((itm) => classCreate(EClassList.ANSWER, itm)) || []; + this.#branch = classCreate(EClassList.BRANCH, data.branch, true); + this.#section = classCreate(EClassList.SECTION, data.section, true); + } + + public get answer() { + return this.#answer; + } + + public set answer(val: string) { + this.#answered.push(val); + this.#answer = val; + } + + public getAnswerHistory() { + return [...this.#answered]; + } + + public get branch() { + return this.#branch; + } + + public set branch(val: BranchCore | undefined) { + this.#branch = val; + } + + public get answers() { + return this.#answers; + } + + public get section() { + return this.#section; + } + + public set section(val: SectionCore | undefined) { + this.#section = val; + } + + public get type(): TQuestionType { + return this.#type; + } + + public override existsIn(data: TCollectable, direction?: TPointerDirection): boolean { + if (super.existsIn(data, direction)) { + return true; + } + return existsInPool(data, this); + } + + public override add(data: TCollectable, direction?: TPointerDirection): QuestionCore { + if (this.existsIn(data, direction)) { + return this; + } + addToPool(data, this); + if (data instanceof AnswerCore) { + data.add(this); + } + return this; + } +} diff --git a/packages/questionable-core/src/composable/QuestionableConfigCore.ts b/packages/questionable-core/src/composable/QuestionableConfigCore.ts deleted file mode 100644 index 84ca8f60..00000000 --- a/packages/questionable-core/src/composable/QuestionableConfigCore.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { isString, merge } from 'lodash'; -import { EventEmitterCore } from './EventEmitterCore'; -import { - isEnum, MODE, noop, TStringDictionaryCore, -} from '../util'; -import { - INavigationConfigCore, - IPagesConfigCore, - IProgressBarConfigCore, - IQuestionableConfigCore, - IQuestionConfigCore, - IStepConfigCore, -} from '../survey/IQuestionableConfigCore'; - -/** - * Configuration class for customizing the Questionable components - * - * The config has opinionated defaults, but is easily modified using Partial updates - */ -export class QuestionableConfigCore implements IQuestionableConfigCore { - protected _mode = MODE.VIEW; - - protected _nav: INavigationConfigCore = {}; - - protected _pages: IPagesConfigCore = {}; - - protected _progressBar: IProgressBarConfigCore = {}; - - protected _questions: IQuestionConfigCore = {}; - - protected _steps: IStepConfigCore = {}; - - protected _events: EventEmitterCore = new EventEmitterCore({ - onActionClick: noop, - onAnswer: noop, - onAnyEvent: noop, - onBranch: noop, - onError: noop, - onGateSwitch: noop, - onInit: noop, - onNoResults: noop, - onPage: noop, - onResults: noop, - }); - - protected _params: TStringDictionaryCore = {}; - - constructor(config: Partial = {}) { - merge(this, config); - if (config.getRuntimeConfig) { - this._params = config.getRuntimeConfig(); - } - if (this._params.dev) { - this._mode = MODE.DEV; - } - } - - get dev(): boolean { - return this._mode === MODE.DEV; - } - - get events(): EventEmitterCore { - return this._events; - } - - set events(val: Partial) { - merge(this._events, val); - } - - get mode(): MODE { - return this._mode; - } - - set mode(val: MODE | string) { - if (isString(val)) { - if (isEnum(MODE, val)) { - this._mode = val as MODE; - } else { - this._mode = MODE.VIEW; - } - } else { - this._mode = val; - } - } - - get params(): TStringDictionaryCore { - return this._params || {}; - } - - get nav(): INavigationConfigCore { - return { ...this._nav }; - } - - set nav(val: Partial) { - merge(this._nav, val); - } - - get pages(): IPagesConfigCore { - return this._pages; - } - - set pages(val: Partial) { - merge(this._pages, val); - } - - get progressBar(): IProgressBarConfigCore { - return { ...this._progressBar }; - } - - set progressBar(val: Partial) { - merge(this._progressBar, val); - } - - get questions(): IQuestionConfigCore { - return { ...this._questions }; - } - - set questions(val: Partial) { - merge(this._questions, val); - } - - get steps(): IStepConfigCore { - return { ...this._steps }; - } - - set steps(val: Partial) { - merge(this._steps, val); - } -} diff --git a/packages/questionable-core/src/composable/QuestionnaireCore.ts b/packages/questionable-core/src/composable/QuestionnaireCore.ts index 372f2a54..b12f9cab 100644 --- a/packages/questionable-core/src/composable/QuestionnaireCore.ts +++ b/packages/questionable-core/src/composable/QuestionnaireCore.ts @@ -1,757 +1,177 @@ -import { ArrayUnique } from 'class-validator'; -import { groupBy, isEmpty, merge } from 'lodash'; -import { IActionCore } from '../survey/IActionCore'; -import { IBranchCore } from '../survey/IBranchCore'; -import { IFormCore } from '../survey/IFormCore'; -import { IPagesCore } from '../survey/IPagesCore'; -import { IQuestionnaireCore } from '../survey/IQuestionnaireCore'; -import { IResultCore } from '../survey/IResultCore'; -import { IStepDataCore } from '../survey/IStepDataCore'; -import { log } from '../util/log'; -import { matches } from '../util/helpers'; -import { QuestionableConfigCore } from './QuestionableConfigCore'; -import { TAgeCore, TAgeCalcCore } from '../util/types'; +/* eslint-disable import/no-cycle */ +import { ActionCore } from './ActionCore'; +import { StepCore } from './StepCore'; +import { IQuestionnaireCore } from '../metadata/IQuestionnaireCore'; +import { QuestionableConfigCore } from './ConfigCore'; +import { PagesCore } from './PagesCore'; import { - ACTION, - DIRECTION, - isEnum, - MODE, - PAGE_TYPE, - PROGRESS_BAR_STATUS, - QUESTION_TYPE, - STEP_TYPE, -} from '../util/enums'; + checkInstanceOf, + ClassList, + TInstanceOf, +} from '../lib/instanceOf'; +import { ResultCore } from './ResultCore'; +import { matches } from '../lib/helpers'; import { - IPageCore, - IQuestionCore, - IRequirementCore, - IResponseCore, - ISectionCore, - IStepCore, -} from '../survey/IStepCore'; -import { IPageConfigCore } from '../survey'; - -export interface IQuestionableCore { - questionnaire: QuestionnaireCore, -} + addToPool, +} from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { RefCore } from './RefCore'; +import { QuestionCore } from './QuestionCore'; +import { BranchCore } from './BranchCore'; +import { SectionCore } from './SectionCore'; +import { TRefType } from '../metadata/properties/type/TRefType'; /** * Utility wrapper for survey state */ -export class QuestionnaireCore implements IQuestionnaireCore { - @ArrayUnique((action: IActionCore) => action.id) - public actions: IActionCore[] = []; - - public branches: IBranchCore[] = []; - - public config: QuestionableConfigCore = new QuestionableConfigCore(); - - public flow: string[] = []; - - public header = ''; - - @ArrayUnique((result: IResultCore) => result.label) - public results: IResultCore[] = []; - - public pages!: IPagesCore; - - @ArrayUnique((question: IQuestionCore) => question.id) - public questions: IQuestionCore[] = []; - - @ArrayUnique((section: ISectionCore) => section.id) - public sections: ISectionCore[] = []; - - protected steps: IStepCore[] = []; - - constructor(data: Partial) { - merge(this, data); - - // Create a new collection for our flow logic - this.steps = this.questions.map((q, i) => ({ - order: i, - ...q, - })); - - this.init(); - - // Wizard flow is defined as linear sequence of unique ids - this.flow = this.steps.map((q) => q.id); +export class QuestionnaireCore extends RefCore implements IQuestionnaireCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.questionnaire; } - /** - * Fetches the first step - * @returns - */ - getFirstStep(): T { - const ret = this.steps[0] as T; - if (!ret) { - this.throw('There is no step'); - } - // if (isEnum(QUESTION_TYPE, ret.type) && (ret instanceof T)) { - // return ret as IQuestionCore; - // } - // if (isEnum(PAGE_TYPE, ret.type)) { - // return ret as IPageCore; - // } - return ret; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override[Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.questionnaire, ClassList.base], obj }); } - /** - * Fetches a question by its id - * @param id unique identifier of the question - * @returns - */ - getStepById(id: string): IStepCore { - const ret = this.steps.find((q) => q.id === id); - if (!ret) { - this.throw(`Step id: ${id} not found in survery`); + public static override create(data: Partial) { + if (data instanceof QuestionnaireCore) { + return data; } - return ret; + return new QuestionnaireCore(data); } - /** - * Fetches a question by its id - * @param id unique identifier of the question - * @returns - */ - getPageById(id: string): IPageCore { - const ret = this.getStepById(id); - if (!isEnum(PAGE_TYPE, ret.type)) { - this.throw(`Step id: ${id} is not a page`); + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; } - return ret as IPageCore; + return QuestionnaireCore.create(data); } - /** - * Fetches a question by its id - * @param id unique identifier of the question - * @returns - */ - getQuestion(q: Partial): IQuestionCore { - if (!q.id) { - this.throw(`Question ${q} is not defined`); - } - return this.getQuestionById(q.id); - } + #config!: QuestionableConfigCore; - /** - * Fetches a question by its id - * @param id unique identifier of the question - * @returns - */ - getQuestionById(id: string): IQuestionCore { - const ret = this.getStepById(id); - if (!isEnum(QUESTION_TYPE, ret.type)) { - this.throw(`Step id: ${id} not a question`); - } - return ret as IQuestionCore; - } + #pages!: PagesCore; - // protected isValidExit(question: IQuestionCore, form: IFormCore, skip = 0) { - // let allowExit = true; - // if (skip === 0 - // && direction === DIRECTION.FORWARD - // && thisQuestion.exitRequirements - // && thisQuestion.exitRequirements.length > 0) { - // allowExit = thisQuestion.exitRequirements.every((r) => - // this.meetsAllRequirements(r, form)); - // } - // return allowExit; - // } - - /** - * Returns the next step in the sequence which is permitted by the current state of the form - */ - // eslint-disable-next-line sonarjs/cognitive-complexity - getStep(thisStep: string, form: IFormCore, direction: DIRECTION, skip = 0): string { - const nextStep = this.flow.indexOf(thisStep) !== -1 - ? this.flow[this.flow.indexOf(thisStep) + direction] - : undefined; - // If there are no more steps, stay on current - if (!nextStep) { - return thisStep; - } + #questions!: QuestionCore[]; - const thisQuestion = this.getStepById(thisStep); - if (skip === 0 - && direction === DIRECTION.FORWARD - && thisQuestion.exitRequirements - && thisQuestion.exitRequirements.length > 0) { - const allowExit = thisQuestion.exitRequirements.every((r) => - this.meetsAllRequirements(r, form)); - if (!allowExit) { - return thisStep; - } - } + #actions!: ActionCore[]; - if (this.config.mode === MODE.EDIT) { - return nextStep; - } - // Special handling for results - const hasResults = this.getResults(form).length > 0; - if (nextStep === STEP_TYPE.RESULTS && !hasResults) { - return STEP_TYPE.NO_RESULTS; - } - if (nextStep === STEP_TYPE.NO_RESULTS && hasResults) { - return STEP_TYPE.RESULTS; - } - - const nextQuestion = this.getStepById(nextStep); - if (!nextQuestion?.entryRequirements) { - return nextStep; - } - - // match is a tri-state (undefined === unset) - let match: boolean | undefined; - - // Each requirement is joined by `OR` - nextQuestion.entryRequirements?.forEach((r) => { - // This safely handles cases where requirement parameters are undefined - const next = this.meetsAllRequirements(r, form); - - if (match === undefined) { - match = next; - } else { - match = match || next; - } - }); - - // If the requested step meets all requirements, return it - if (match) { - return nextStep; - } - // Get the next step whose requirements are met - const n = this.getStep(nextStep, form, direction, skip + 1); - if (n !== nextStep) { - return n; - } - return thisStep; - } + #steps!: StepCore[]; - /** - * Gets the next allowed step id in the sequence - * @param props step context - * @returns step id - */ - getNextStep(props: IStepDataCore): string { - const thisStep = props.stepId as string; - const dir = DIRECTION.FORWARD; - return this.getStep(thisStep, props.form, dir); - } + #branches!: BranchCore[]; - /** - * Gets the previously answered step id - * @param props step context - * @returns step id - */ - getPreviousStep(props: IStepDataCore): string { - const thisStep = props.stepId as string; - const dir = DIRECTION.BACKWARD; - return this.getStep(thisStep, props.form, dir); - } + #sections!: SectionCore[]; - /** - * Calculate the percent of survey completed - * @param props - * @returns - */ - getProgressPercent(props: IStepDataCore): number { - const stepId = `${props.stepId}`; - const step = this.getStepById(stepId); - if (matches(step.type, PAGE_TYPE.LANDING)) { - // Landing page exists before progress starts - // less than 0% progress can be interpretted as 'do not display' - return -1; - } - if (matches(step.type, PAGE_TYPE.SUMMARY)) { - // However we land on the summary, this is 100% - return 100; - } - if ( - matches(step.type, PAGE_TYPE.RESULTS) - || matches(step.type, PAGE_TYPE.NO_RESULTS) - ) { - // Results are beyond the survery progress - // greater than 100% can be interpretted as 'do not display' - return 101; - } + #flow!: string[]; - // if we have branches then we need to do this, otherwise we need to get the number of steps - const answerable = this.getAllAnswerableQuestions(props); - const lastStep = answerable.length; // sections[sections.length - 1]?.lastStep; + #header!: string; - // if there is no step, the questionnaire has just started - if (lastStep <= 0) { - return 0.1; - } - const thisStepIdx = answerable.indexOf(stepId) + 1; - // add 2 to account for the summary and result steps - let lastStepIdx = lastStep + 2; - if (this.config.mode === MODE.EDIT) { - // if in design mode, every step will be iterated - lastStepIdx = this.flow.length - 1; - } - // To calculate the percent, divide the index of this step - // by the index of the last step multiplied by 100. + #results!: ResultCore[]; - return Math.round((thisStepIdx / lastStepIdx) * 100); + constructor(data: Partial) { + super(data); + this.init(data); } - /** - * Gets all of the questions associated with a branch - * @param props - * @returns string[] - */ - getAllAnswerableQuestions(props: IStepDataCore): string[] { - const stepId = `${props.stepId}`; - const step = this.getStepById(stepId); - if (!isEnum(QUESTION_TYPE, step.type)) { - return []; - } - - if (this.branches.length) { - const question = step as IQuestionCore; - return this.getBranchQuestions(question); - } - return this.getQuestionsWithoutBranches(); + public init(data: Partial) { + const config = (data.config instanceof QuestionableConfigCore) + ? data.config : new QuestionableConfigCore(data.config); + this.#config = config; + this.#pages = PagesCore.create(data.pages || {}); + this.#questions = data.questions?.map((q) => QuestionCore.create(q)) || []; + this.#actions = data.actions?.map((q) => ActionCore.create(q)) || []; + this.#branches = data.branches?.map((b) => BranchCore.create(b)) || []; + this.#sections = data.sections?.map((s) => SectionCore.create(s)) || []; + this.#header = data.header || ''; + this.#results = data.results?.map((r) => ResultCore.create(r)) || []; + this.#steps = this.#questions.map((q) => q) || []; + this.#flow = this.#steps.map((q) => q.id); + return this; } - /** - * Gets all of the questions associated with a branch - * @param step - * @returns string[] - */ - protected getBranchQuestions(step: IQuestionCore): string[] { - const question = step as IQuestionCore; - - if (question.branch) { - this.config.events.gate({ - data: { - [this.header]: `${question.branch.title}`, - }, - gate: 'branch', - }); - - return ( - this.branches - .find((b) => b.id === question.branch?.id) - ?.questions.map((q) => q.id) || [] - ); + public set(obj: T) { + if (obj instanceof QuestionableConfigCore) { + this.#config = obj; } - return []; } - /** - * Gets all of the questions regardless of branch - * @returns string[] - */ - protected getQuestionsWithoutBranches(): string[] { - return this.steps - .filter((q) => isEnum(QUESTION_TYPE, q.type)) - .map((q) => q.id); + public get actions(): ActionCore[] { + return this.#actions; } - /** - * Gets a list of questions that may be answered in the future - * @param props - * @returns - */ - getAnswerableQuestions(props: IStepDataCore): string[] { - return this.questions - .filter( - (q) => - !q.entryRequirements - || q.entryRequirements.length === 0 - || q.entryRequirements.some((r) => - this.meetsAllRequirements(r, props.form, true)), - ) - .map((q) => q.id); + public get branches(): BranchCore[] { + return this.#branches; } - /** - * Gets all of the currently available sections - * @param props - * @returns - */ - getSections(props: IStepDataCore): ISectionCore[] { - if (!props || !this.sections || this.sections.length === 0) { - return []; - } - - const thisStep = props.stepId as string; - const thisQuestion = this.getStepById(thisStep); - const thisQuestionIdx = this.steps.indexOf(thisQuestion); - - // Get all sections that meet the requirements based on current answers - let sections = this.sections.filter( - (s) => - s.requirements.length === 0 - || s.requirements.some((r) => this.meetsAllRequirements(r, props.form)), - ); - if (this.config.mode === MODE.EDIT) { - // In design mode, all sections are valid - sections = [...this.sections]; - } - return sections.map((s) => { - const section = { ...s }; - section.lastStep = this.questions.reduce( - (acc, q, index) => (q.section.id === s.id ? index : acc), - -1, - ); - if (matches(section.id, PAGE_TYPE.RESULTS)) { - section.lastStep = this.questions.length - 2; - } else if (matches(section.id, PAGE_TYPE.LANDING)) { - section.lastStep = 0; - } - if (section.lastStep < 0) { - section.status = PROGRESS_BAR_STATUS.INCOMPLETE; - } else if (matches(section.id, thisQuestion.section.id)) { - section.status = PROGRESS_BAR_STATUS.CURRENT; - } else if (section.lastStep < thisQuestionIdx) { - section.status = PROGRESS_BAR_STATUS.COMPLETE; - } - return section; - }); + public get config(): QuestionableConfigCore { + return this.#config; } - /** - * Get all the results compatible with the current answers of the form - * @param form - * @returns - */ - getResults(form: IFormCore): IResultCore[] { - return this.results.filter((r) => - r.requirements.some((match) => { - if (this.meetsAllRequirements(match, form)) { - Object.assign(r, { match }); - return true; - } - return false; - })); + public get flow(): string[] { + return this.#flow; } - /** - * Gets the appropriate action given a set of results - * @returns - */ - getActionByType(type: ACTION): IActionCore { - const action = this.actions.find((a) => a.type === type); - if (!action) { - this.throw(`No matching action found for ${type}`); - } - return action; + public get header(): string { + return this.#header; } - /** - * Gets the appropriate action given a set of results - * @returns - */ - getAction(results: IResultCore[]): IActionCore { - const groupedByAction = groupBy(results, 'action.id'); - const hybrid = this.actions.find((a) => a.type === ACTION.HYBRID); - // If group above has more than one type of action, the resolved action will be a hybrid - let match = hybrid; - const actions = Object.keys(groupedByAction); - if (actions.length === 1) { - match = this.actions.find((a) => a.id === actions[0]); - if (!match) { - this.throw(`Action id ${actions[0]} could not be found.`); - } - } - if (!match) { - this.throw('Could not find a Call to Action for these results.'); - } - return match; + public get questions(): QuestionCore[] { + return this.#questions; } - /** - * Internal wrapper to create error, log, and throw - * @param e Error as string - */ - protected throw(e: string): never { - const error = new Error(e); - this.config.events.error(error); - throw error; + public get steps(): StepCore[] { + return this.#steps; } - /** - * Ensure the survey is constructed with (minimally) valid data - */ - protected validateInput() { - if (this.questions?.length <= 0) { - this.throw('No questions have been defined.'); - } - if (this.header?.length <= 0) { - this.throw('No header has been defined.'); - } - if (this.results?.length <= 0) { - this.throw('No results have been defined.'); - } + public get sections(): SectionCore[] { + return this.#sections; } - /** - * Ensure we have referential integrity between questions/branches - * In the cases where branches have defined and mismatches exist: - * Top level branches that have questions assigned will update the question reference to use the branch - * Each question that defines a branch relationship will establish a reference to a top level branch - */ - protected syncBranches(): void { - // Branches defined take priority; sync these first - this.branches.forEach((b) => { - b.questions.forEach((bq) => { - const question = this.questions.find((q) => q.id === bq.id); - if (question && question?.branch?.id !== b.id) { - question.branch = b; - } - }); - }); - - this.questions.forEach((q) => { - // Branches are optional on questions - if (!q.branch?.id) { - return; - } - const exists = this.branches.find((b) => b.id === q.branch?.id); - const validateBranch = exists || (q.branch as IBranchCore); - if (!exists) { - this.branches.push(validateBranch); - } - validateBranch.questions = validateBranch.questions || []; - if (!validateBranch.questions.find((bq) => bq.id === q.id)) { - validateBranch.questions.push(q); - } - }); + public get results(): ResultCore[] { + return this.#results; } - /** - * Gets the data and configuration for a page type - * @param type Page Type - * @returns - */ - // eslint-disable-next-line class-methods-use-this - protected getPageSet = ( - type: PAGE_TYPE, - ): { - config?: Partial; - data?: IPageCore; - } => { - const data: IPageCore = Object.values(this.pages).find((p: IPageCore) => p.type === type); - if (data) { - return { - config: {}, - data, - }; - } - return { - config: {}, - data: { - id: '-1', - section: {}, - type, - }, - }; - }; - - /** - * If configured, sets a default page if required for the page type - * @param idx index of the page in `this.steps` - * @param type LANDING, RESULTS, etc - */ - protected setPage(idx: number, type: PAGE_TYPE): void { - const error = 'step is not correctly defined or defined more than once'; - - const page = this.getPageSet(type); - // visible is truthy unless explicitly set to false - if (!page.data || page?.config?.visible === false) { - if (this.steps.filter((q) => q.type === type).length > 0) { - // if the page has been assigned to steps, remove it - const doomed = this.steps.find((q) => q.type === type); - if (doomed) { - const doomIdx = this.steps.indexOf(doomed); - this.steps.splice(doomIdx, 1); - } - } - return; - } - // Ensure the wizard has this page at the specified location - if (this.steps[idx].type !== type) { - if (idx === 0) { - // In the case of the landing page, it will always go first - this.steps.unshift(page.data); - } else { - this.steps.splice(idx + 1, 0, page.data); - } - } - if (this.steps.filter((q) => q.type === type).length !== 1) { - this.throw(`${type} ${error}.`); - } + public get pages(): PagesCore { + return this.#pages; } - /** - * Sets step defaults for landing, summary and results if none are defined. - */ - protected setPageDefaults(): void { - this.setPage(0, PAGE_TYPE.LANDING); - this.setPage(this.steps.length - 1, PAGE_TYPE.SUMMARY); - this.setPage(this.steps.length - 1, PAGE_TYPE.RESULTS); - this.setPage(this.steps.length - 1, PAGE_TYPE.NO_RESULTS); + public get id(): string { + return '1'; } - /** - * Performs constructor validation on the survery inputs. - */ - protected init(): void { - this.validateInput(); - this.syncBranches(); - this.setPageDefaults(); + public get label() { + return ''; } - public meetsAllRequirements( - requirement: IRequirementCore, - form: IFormCore, - allowUnanswered = false, - ) { - const { - minAge, maxAge, responses: answers, ageCalc, - } = requirement; - // Internal to each requirement, all evaluations are `AND` - // This safely handles cases where requirement parameters are undefined - return ( - QuestionnaireCore.meetsMinAgeRequirements(form, minAge) - && QuestionnaireCore.meetsMaxAgeRequirements(form, maxAge) - && QuestionnaireCore.meetsAgeCalcRequirements(form, ageCalc) - && this.meetsAnswerRequirements(answers, allowUnanswered) - ); + public get title(): string { + return this.header; } - /** - * Validates minimum age requirements - * @param form The current state of the form - * @param minAge a TAge object or undefined - * @returns true if no min age, else true if age is >= min age - */ - public static meetsMinAgeRequirements(form: IFormCore, minAge?: TAgeCore): boolean { - if (!minAge) return true; - - if (form.age === undefined) { - return false; - } - const { - age: { years, months }, - } = form; - - return ( - years > minAge?.years - || (years >= minAge?.years && months >= minAge?.months) - ); + public get type(): TRefType { + return 'default'; } - /** - * Validates maximum age requirements - * @param form The current state of the form - * @param maxAge a TAge object or undefined - * @returns true if no max age, else true if age is <= max age - */ - public static meetsMaxAgeRequirements(form: IFormCore, maxAge?: TAgeCore): boolean { - if (!maxAge) return true; - if (form.age === undefined) { - return false; + public existsIn(data: TCollectable): boolean { + if (data instanceof QuestionCore) { + return this.#questions.some((q) => q === data || matches(q.title, data.title)); } - const { - age: { years, months }, - } = form; - - return ( - years < maxAge?.years - || (years <= maxAge?.years && months <= maxAge?.months) - ); - } - - /** - * Executes an arbitrary function to determine age eligibility - * @param form The current state of the form - * @param ageCalc A callback function that operates on an age - * @returns - */ - protected static meetsAgeCalcRequirements( - form: IFormCore, - ageCalc?: TAgeCalcCore, - ): boolean { - if (!ageCalc) return true; - if (form.birthdate === undefined) { - return false; + if (data instanceof BranchCore) { + return this.#branches.some((q) => q === data || matches(q.title, data.title)); } - const { birthdate } = form; - - return ageCalc(birthdate); - } - - /** - * - * @param questionAnswer the user's answer - * @param matchAnswer the question answer for validation - * @param allowUnanswered optional bit to ignore undefined - * @param id answer id - * @returns true if the answer is a match - */ - protected matchesAnswer( - questionAnswer?: string, - matchAnswer?: string, - allowUnanswered = false, - id = '', - ): boolean { - let ret = false; - if (allowUnanswered && isEmpty(questionAnswer)) { - ret = true; + if (data instanceof SectionCore) { + return this.#sections.some((q) => q === data || matches(q.title, data.title)); } - if (isEmpty(questionAnswer)) { - ret = false; + if (data instanceof ResultCore) { + return this.#results.some((q) => q === data || matches(q.title, data.title)); } - ret = matches(questionAnswer, matchAnswer); - if (this.config.dev) { - log('Answer matching', { - id, - matchAnswer, - questionAnswer, - ret, - }); + if (data instanceof ActionCore) { + return this.#actions.some((q) => q === data || matches(q.title, data.title)); } - return ret; + return false; } - /** - * Determines if current answers in the form meet the step's requirements - * @param answers Collection of required answer that `matches()` - * @param allowUnanswered if true, consider questions that are not yet answered - * @returns true if all answers are valid or if no answers are required - */ - protected meetsAnswerRequirements( - answers?: IResponseCore[], - allowUnanswered = false, - ): boolean { - if (!answers || answers.length <= 0) return true; - - return answers.every((a) => { - const question = this.getQuestion(a.question); - if (question.answers?.length > 0) { - // Allowed answers are an array. Any matched answer makes the response valid. - const hasAnyMatch = a.answers.some((i) => { - const answer = question.answers.find((x) => - matches(`${x.id}`, `${i.id}`)); - return this.matchesAnswer( - question.answer, - answer?.title, - allowUnanswered, - i.id, - ); - }); - if (this.config.dev) { - log('Answer matching', { hasAnyMatch, question }); - } - return hasAnyMatch; - } - // If no answers are defined, this passes - return true; - }); + public add(data: TCollectable): QuestionnaireCore { + addToPool(data, this); + return this; } } diff --git a/packages/questionable-core/src/composable/RefCore.ts b/packages/questionable-core/src/composable/RefCore.ts new file mode 100644 index 00000000..cd5691a2 --- /dev/null +++ b/packages/questionable-core/src/composable/RefCore.ts @@ -0,0 +1,125 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { TRefCoreProperties } from '../metadata/properties/MRef'; +import { IRefCore } from '../metadata/IRefCore'; +import { REF_TYPE, TRefType } from '../metadata/properties/type/TRefType'; +import { checkInstanceOf, ClassList, TInstanceOf } from '../lib/instanceOf'; +import { getGUID } from '../lib/uuid'; +import { BaseCore } from './BaseCore'; + +/** + * Base class for all objects that should be stored by reference, + * most commonly because they represent unique rows in a table or + * distinct, complex structures whose value can be inferred by unique identfiers + */ +export class RefCore extends BaseCore implements IRefCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.ref; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.ref], obj }); + } + + // public static isRef(data: any): data is RefCore { + // return 'title' in data; + // } + + // public static is(data: any): data is T { + // if (T === RefCore) { + + // } + // return 'title' in data; + // } + + // public isRef(data: any): data is RefCore { + // return RefCore.isRef(this); + // } + + public static override create(data: Partial) { + if (data instanceof RefCore) { + return data; + } + return new RefCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return RefCore.create(data); + } + + #_id: string; + + #_label: string; + + #_type: TRefType; + + #_title: string; + + /** + * Instantiation will generate a uuid for this object + * @param data optional data + */ + public constructor(data: Partial) { + super(data); + if (data.id && data.id.length > 0) { + this.#_id = data.id; + } else { + this.#_id = getGUID({ short: true }); + } + this.#_label = data.label || ''; + this.#_title = data.title || ''; + this.#_type = data.type || REF_TYPE.DEFAULT; + } + + public get id(): string { + return this.#_id; + } + + public get label() { + return this.#_label; + } + + public get title(): string { + return this.#_title; + } + + public get type(): TRefType { + return this.#_type; + } + + public set(prop: TRefCoreProperties, val: T) { + this[`#_${prop}`] = val; + } + + public existsIn(data: TCollectable): boolean { + return existsInPool(data, this); + } + + public add(data: TCollectable): RefCore { + addToPool(data, this); + return this; + } + // /** + // * Root class has no collections, will always be false + // * @param val + // * @returns + // */ + // public existsIn(val: RefCore): boolean { + // this.noop(val); + // return false; + // } + + // /** + // * This does nothing but establish the pattern for all other classes to build from; + // * @param data any questionable object + // */ + // public add(data: RefCore): RefCore { + // this.noop(data); + // return this; + // } +} diff --git a/packages/questionable-core/src/composable/RequirementCore.ts b/packages/questionable-core/src/composable/RequirementCore.ts new file mode 100644 index 00000000..0dbc7642 --- /dev/null +++ b/packages/questionable-core/src/composable/RequirementCore.ts @@ -0,0 +1,96 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { IRequirementCore } from '../metadata/IRequirementCore'; +import { REQUIREMENT_TYPE, TRequirementType } from '../metadata/properties/type/TRequirementType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { TAgeCalcCore } from '../lib/types'; +import { TAgeCore } from '../metadata/types/TAgeCore'; +import { RefCore } from './RefCore'; +import { ResponseCore } from './ResponseCore'; +import { classCreate } from '../constructable/Factory'; + +export class RequirementCore extends RefCore implements IRequirementCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.requirement; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.requirement, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof RequirementCore) { + return data; + } + return new RequirementCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return RequirementCore.create(data); + } + + #ageCalc; + + #explanation; + + #maxAge; + + #minAge; + + #responses; + + #type: TRequirementType; + + constructor(data: Partial) { + super(data); + this.#ageCalc = data.ageCalc || (() => true); + this.#explanation = data.explanation || ''; + this.#maxAge = data.maxAge; + this.#minAge = data.minAge; + this.#responses = data.responses?.map((itm) => classCreate(EClassList.RESPONSE, itm)) || []; + this.#type = data.type || REQUIREMENT_TYPE.NON_REQUIRED; + } + + get ageCalc(): TAgeCalcCore | undefined { + return this.#ageCalc; + } + + get explanation(): string { + return this.#explanation; + } + + get maxAge(): TAgeCore | undefined { + return this.#maxAge; + } + + get minAge(): TAgeCore | undefined { + return this.#minAge; + } + + get responses(): ResponseCore[] { + return this.#responses; + } + + get type(): TRequirementType { + return this.#type; + } + + public existsIn(data: TCollectable): boolean { + return existsInPool(data, this); + } + + public add(data: TCollectable): RequirementCore { + addToPool(data, this); + if (data.instanceOfCheck === ClassList.response && data.add) { + data.add(this); + } + return this; + } +} diff --git a/packages/questionable-core/src/composable/ResponseCore.ts b/packages/questionable-core/src/composable/ResponseCore.ts new file mode 100644 index 00000000..6a6aa037 --- /dev/null +++ b/packages/questionable-core/src/composable/ResponseCore.ts @@ -0,0 +1,90 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { IResponseCore } from '../metadata/IResponseCore'; +import { RESPONSE_TYPE, TResponseType } from '../metadata/properties/type/TResponseType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { AnswerCore } from './AnswerCore'; +import { QuestionCore } from './QuestionCore'; +import { RefCore } from './RefCore'; +import { classCreate } from '../constructable/Factory'; + +type TMatches = { + question: TCollectable; +} | { + answers: TCollectable[]; +}; +type TRespondable = TCollectable & TMatches; + +export class ResponseCore extends RefCore implements IResponseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.response; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.response], obj }); + } + + public static override create(data: Partial) { + if (data instanceof ResponseCore) { + return data; + } + return new ResponseCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return ResponseCore.create(data); + } + + #answers: AnswerCore[]; + + #question: QuestionCore | undefined; + + #type: TResponseType; + + constructor(data: Partial) { + super(data); + this.#answers = data.answers?.map((itm) => classCreate(EClassList.ANSWER, itm)) || []; + this.#question = classCreate(EClassList.QUESTION, data.question, true); + this.#type = data.type || RESPONSE_TYPE.COMPLETE; + } + + public get question(): QuestionCore | undefined { + return this.#question; + } + + public set question(data: QuestionCore | undefined) { + if (data && !this.#question) { + this.#question = data; + } + } + + public get answers(): AnswerCore[] { + return this.#answers; + } + + public set answers(data: AnswerCore[]) { + if (!this.#answers) { + this.#answers = data; + } + } + + public get type(): TResponseType { + return this.#type; + } + + public existsIn(data: TRespondable): boolean { + return existsInPool(data, this); + } + + public add(data: TCollectable): ResponseCore { + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/ResultCore.ts b/packages/questionable-core/src/composable/ResultCore.ts new file mode 100644 index 00000000..5c2e9ff4 --- /dev/null +++ b/packages/questionable-core/src/composable/ResultCore.ts @@ -0,0 +1,135 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { IResultCore } from '../metadata/IResultCore'; +import { RESULT_TYPE, TResultType } from '../metadata/properties/type/TResultType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { ActionCore } from './ActionCore'; +import { RefCore } from './RefCore'; +import { RequirementCore } from './RequirementCore'; +import { classCreate } from '../constructable/Factory'; +import { TCollectable } from '../metadata/types/TCollectable'; + +export class ResultCore extends RefCore implements IResultCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.result; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.result], obj }); + } + + // public static isResult(data: any): data is ResultCore { + // return 'title' in data; + // } + + // public isRef(data: any): data is RefCore { + // return ResultCore.isRef(this); + // } + + public static override create(data: Partial) { + if (data instanceof ResultCore) { + return data; + } + return new ResultCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return ResultCore.create(data); + } + + #action: ActionCore | undefined; + + #category: string; + + #label: string; + + #match: RequirementCore | undefined; + + #order: number; + + #reason: string; + + #requirements: RequirementCore[]; + + #secondaryAction: ActionCore | undefined; + + #type: TResultType; + + constructor(data: Partial) { + super(data); + const type: TResultType = (!data.type || `${data.type}` === `${RESULT_TYPE.DEFAULT}`) + ? RESULT_TYPE.MATCH : data.type; + this.#type = type; + this.#action = classCreate(EClassList.ACTION, data.action, true); + this.#match = classCreate(EClassList.REQUIREMENT, data.match, true); + this.#requirements = data.requirements?.map((itm) => classCreate(EClassList.REQUIREMENT, itm)) || []; + this.#secondaryAction = classCreate(EClassList.ACTION, data.secondaryAction, true); + this.#reason = data.reason || ''; + this.#label = data.label || ''; + this.#category = data.category || ''; + this.#order = data.order || 0; + } + + public get action(): ActionCore | undefined { + return this.#action; + } + + public get category() { + return this.#category; + } + + public get label(): string { + return this.#label; + } + + public get match() { + return this.#match; + } + + public set match(val: RequirementCore | undefined) { + this.#match = val; + } + + public get reason(): string { + return this.#reason; + } + + public set reason(val: string) { + this.#reason = val; + } + + public get requirements(): RequirementCore[] { + return this.#requirements; + } + + public set requirements(val: RequirementCore[]) { + this.#requirements = val; + } + + public get secondaryAction() { + return this.#secondaryAction; + } + + public get order() { + return this.#order; + } + + public get type(): TResultType { + return this.#type; + } + + public existsIn(data: TCollectable): boolean { + return existsInPool(data, this); + } + + public add(data: TCollectable): ResultCore { + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/SectionCore.ts b/packages/questionable-core/src/composable/SectionCore.ts new file mode 100644 index 00000000..63f156f9 --- /dev/null +++ b/packages/questionable-core/src/composable/SectionCore.ts @@ -0,0 +1,102 @@ +/* eslint-disable import/no-cycle */ +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { + ISectionCore, +} from '../metadata/ISectionCore'; +import { + SECTION_TYPE, + TSectionType, +} from '../metadata/properties/type/TSectionType'; +import { PROGRESS_BAR_STATUS, TProgressBarStatusType } from '../metadata/types/TProgressBarStatusType'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { RefCore } from './RefCore'; +import { RequirementCore } from './RequirementCore'; +import { classCreate } from '../constructable/Factory'; +import { TCollectable } from '../metadata/types/TCollectable'; + +export class SectionCore extends RefCore implements ISectionCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.section; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.section, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof SectionCore) { + return data; + } + return new SectionCore(data); + } + + public static override createOptional(data?: Partial) { + if (!data || !super.createOptional(data)) { + return undefined; + } + return SectionCore.create(data); + } + + #lastStep: number | undefined; + + #requirements: RequirementCore[]; + + #status: TProgressBarStatusType; + + #order: number; + + #type: TSectionType; + + constructor(data: Partial) { + super(data); + this.#requirements = data.requirements?.map((itm) => classCreate(EClassList.REQUIREMENT, itm)) || []; + this.#lastStep = data.lastStep; + this.#order = data.order || 0; + this.#status = data.status || PROGRESS_BAR_STATUS.INCOMPLETE; + this.#type = data.type || SECTION_TYPE.UNLOCKED; + } + + public get requirements(): RequirementCore[] { + return this.#requirements; + } + + public get lastStep(): number | undefined { + return this.#lastStep; + } + + public set lastStep(val: number | undefined) { + this.#lastStep = val; + } + + public get order(): number { + return this.#order; + } + + public set order(val: number) { + this.#order = val; + } + + public get status(): TProgressBarStatusType { + return this.#status; + } + + public set status(val: TProgressBarStatusType) { + this.#status = val; + } + + public get type(): TSectionType { + return this.#type; + } + + public existsIn(data: TCollectable): boolean { + return existsInPool(data, this); + } + + public add(data: TCollectable): SectionCore { + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/StepCore.ts b/packages/questionable-core/src/composable/StepCore.ts new file mode 100644 index 00000000..3edf9a25 --- /dev/null +++ b/packages/questionable-core/src/composable/StepCore.ts @@ -0,0 +1,157 @@ +/* eslint-disable import/no-cycle */ +import { kebabCase } from 'lodash'; +import { addToPool, existsInPool } from '../constructable/lib/pools'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { PAGE_TYPE } from '../metadata/properties/type/TPageType'; +import { QUESTION_TYPE } from '../metadata/properties/type/TQuestionType'; +import { IStepCore } from '../metadata/IStepCore'; +import { STEP_TYPE, TStepType } from '../metadata/properties/type/TStepType'; +import { isEnum } from '../lib/enums'; +import { matches } from '../lib/helpers'; +import { + checkInstanceOf, ClassList, EClassList, TInstanceOf, +} from '../lib/instanceOf'; +import { TPointerDirection } from '../lib/types'; +import { RefCore } from './RefCore'; +import { RequirementCore } from './RequirementCore'; +import { SectionCore } from './SectionCore'; +import { classCreate } from '../constructable/Factory'; + +export class StepCore extends RefCore implements IStepCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.step; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static override [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.step, ClassList.ref], obj }); + } + + public static override create(data: Partial) { + if (data instanceof StepCore) { + return data; + } + return new StepCore(data); + } + + #entryRequirements: RequirementCore[]; + + #exitRequirements: RequirementCore[]; + + #footer: string; + + #info: string; + + #internalNotes: string; + + #order: number; + + #section: SectionCore | undefined; + + #subTitle: string; + + #type: TStepType; + + constructor(data: Partial) { + super(data); + this.#entryRequirements = data.entryRequirements?.map((itm) => classCreate(EClassList.REQUIREMENT, itm)) || []; + this.#exitRequirements = data.exitRequirements?.map((itm) => classCreate(EClassList.REQUIREMENT, itm)) || []; + const type: TStepType = (!data.type || `${data.type}` === `${STEP_TYPE.DEFAULT}`) ? STEP_TYPE.DEFAULT : data.type; + this.#type = type; + this.#footer = data.footer || ''; + this.#info = data.info || ''; + this.#internalNotes = data.internalNotes || ''; + this.#order = data.order || 0; + this.#section = classCreate(EClassList.SECTION, data.section, true); + this.#subTitle = data.subTitle || ''; + } + + public toString() { + return this.id; + } + + public get entryRequirements(): RequirementCore[] { + return this.#entryRequirements; + } + + public get exitRequirements(): RequirementCore[] { + return this.#exitRequirements; + } + + public get footer(): string { + return this.#footer; + } + + public get info(): string { + return this.#info; + } + + public get internalNotes(): string { + return this.#internalNotes; + } + + public get order(): number { + return this.#order; + } + + public get section(): SectionCore | undefined { + return this.#section; + } + + public get subTitle(): string { + return this.#subTitle; + } + + public get type(): TStepType { + return this.#type; + } + + public getFieldSetName(): string { + return kebabCase(this.title); + } + + public getDomId(answer: string): string { + const name = this.getFieldSetName(); + return `${name}-${kebabCase(answer)}`; + } + + public getStepType() { + if (isEnum({ enm: QUESTION_TYPE, value: this.type })) { + return 'question'; + } + if (isEnum({ enm: PAGE_TYPE, value: this.type })) { + return 'page'; + } + return 'unknown'; + } + + public existsIn(data: TCollectable, direction?: TPointerDirection): boolean { + if (data instanceof RequirementCore) { + if (direction === 'out') { + return this.#exitRequirements.some( + (q) => q === data || matches(q.title, data.title), + ); + } + return this.#entryRequirements.some( + (q) => q === data || matches(q.title, data.title), + ); + } + return existsInPool(data, this); + } + + public add(data: TCollectable, direction?: TPointerDirection): StepCore { + const exists = this.existsIn(data, direction); + if (exists) { + return this; + } + if (data instanceof RequirementCore) { + if (direction === 'out') { + this.#exitRequirements.push(data); + } else { + this.#entryRequirements.push(data); + } + } + addToPool(data, this); + return this; + } +} diff --git a/packages/questionable-core/src/composable/TCtor.ts b/packages/questionable-core/src/composable/TCtor.ts new file mode 100644 index 00000000..6b4cd1ed --- /dev/null +++ b/packages/questionable-core/src/composable/TCtor.ts @@ -0,0 +1,17 @@ +import { ActionCore as Action } from './ActionCore'; +import { AnswerCore as Answer } from './AnswerCore'; +import { BaseCore as Base } from './BaseCore'; +import { BranchCore as Branch } from './BranchCore'; +import { FormCore as Form } from './FormCore'; +import { QuestionCore as Question } from './QuestionCore'; + +export type TCtor = { new (data: Partial): T }; +export type TCtorQuestion = { new (data: Partial): T }; +export type TCtorAction = { new (data: Partial): T }; +export type TCtorAnswer = { new (data: Partial): T }; +export type TCtorBranch = { new (data: Partial): T }; +export type TCtorForm = { new (data: Partial): T }; + +export function create(Ctor: TCtor, data: Partial): T { + return new Ctor(data); +} diff --git a/packages/questionable-core/src/composable/_exports.ts b/packages/questionable-core/src/composable/_exports.ts new file mode 100644 index 00000000..d1279029 --- /dev/null +++ b/packages/questionable-core/src/composable/_exports.ts @@ -0,0 +1,21 @@ +export * from './ActionCore'; +export * from './AnswerCore'; +export * from './BaseCore'; +export * from './BranchCore'; +export * from './ButtonCore'; +export * from './config/_exports'; +export * from './ConfigCore'; +export * from './Dictionary'; +export * from './EventEmitterCore'; +export * from './FormCore'; +export * from './PageCore'; +export * from './PagesCore'; +export * from './QuestionCore'; +export * from './QuestionnaireCore'; +export * from './RefCore'; +export * from './ResultCore'; +export * from './RequirementCore'; +export * from './ResponseCore'; +export * from './SectionCore'; +export * from './StepCore'; +export * from './TCtor'; diff --git a/packages/questionable-core/src/composable/config/ButtonConfig.ts b/packages/questionable-core/src/composable/config/ButtonConfig.ts new file mode 100644 index 00000000..44919475 --- /dev/null +++ b/packages/questionable-core/src/composable/config/ButtonConfig.ts @@ -0,0 +1,87 @@ +import { BaseCore } from '../BaseCore'; +import { + TVerticalPositionCore, + VERTICAL_POSITION, +} from '../../metadata/types/TVerticalPositionCore'; +import { THorizontalPositionCore, HORIZONTAL_POSITION } from '../../metadata/types/THorizontalPositionCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; +import { BUTTON_TYPE, TButtonType } from '../../metadata/properties/type/TButtonType'; + +/** + * Configuration for buttons + */ +export class ButtonConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof ButtonConfigCore) { + return data; + } + return new ButtonConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + this.title = data.title || ''; + this.type = data.type || BUTTON_TYPE.BUTTON; + this.horizontalPos = data.horizontalPos || HORIZONTAL_POSITION.LEFT; + this.verticalPos = data.verticalPos || VERTICAL_POSITION.BOTTOM; + this.visible = data.visible !== false; + this.defaultLabel = data.defaultLabel || this.title; + } + + /** + * Default text to display if none is defined + */ + defaultLabel?: string | undefined; + + /** + * Horizontal orientation (left or right) + * + * @title Horizontal Position + * @default left + */ + horizontalPos?: THorizontalPositionCore; + + /** + * Render mode (link or button) + * + * @title Mode + */ + type?: TButtonType; + + /** + * Vertical orientation (top or bottom) + * + * @title Vertical Position + */ + verticalPos?: TVerticalPositionCore; + + /** + * Toggle whether button is visible + * + * @title Visible + */ + visible?: boolean | undefined; + + /** + * Default link to tie to button click + * + * @title Link + */ + link?: string | undefined; + + /** + * Default button name + * + * @title Title + */ + title?: string; +} diff --git a/packages/questionable-core/src/composable/config/NavigationConfig.ts b/packages/questionable-core/src/composable/config/NavigationConfig.ts new file mode 100644 index 00000000..17edcda1 --- /dev/null +++ b/packages/questionable-core/src/composable/config/NavigationConfig.ts @@ -0,0 +1,48 @@ +import { merge } from 'lodash'; +import { ButtonConfigCore } from './ButtonConfig'; +import { BaseCore } from '../BaseCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; + +/** + * Configuration for navigation + */ +export class NavigationConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof NavigationConfigCore) { + return data; + } + return new NavigationConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + merge(this, data); + if (data.next) { + this.next = ButtonConfigCore.create(data.next); + } + if (data.prev) { + this.prev = ButtonConfigCore.create(data.prev); + } + } + + /** + * Next/Forward button + */ + next?: Partial | undefined; + + /** + * Previous/Go back button + */ + prev?: Partial | undefined; + + visible?: boolean | undefined; +} diff --git a/packages/questionable-core/src/composable/config/PagesConfig.ts b/packages/questionable-core/src/composable/config/PagesConfig.ts new file mode 100644 index 00000000..e3cbae0e --- /dev/null +++ b/packages/questionable-core/src/composable/config/PagesConfig.ts @@ -0,0 +1,30 @@ +import { merge } from 'lodash'; +import { BaseCore } from '../BaseCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; +/** + * Configuration for Pages display + */ +export class PagesConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof PagesConfigCore) { + return data; + } + return new PagesConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + merge(this, data); + } + + visible?: boolean | undefined; +} diff --git a/packages/questionable-core/src/composable/config/ProgressBarConfig.ts b/packages/questionable-core/src/composable/config/ProgressBarConfig.ts new file mode 100644 index 00000000..27aa6c56 --- /dev/null +++ b/packages/questionable-core/src/composable/config/ProgressBarConfig.ts @@ -0,0 +1,75 @@ +import { merge } from 'lodash'; +import { TVerticalPositionCore } from '../../metadata/types/TVerticalPositionCore'; +import { BaseCore } from '../BaseCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; +import { TProgressBarStatusType } from '../../metadata/types/TProgressBarStatusType'; + +/** + * Configuration options for the progress bar + */ +export class ProgressBarConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof ProgressBarConfigCore) { + return data; + } + return new ProgressBarConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + merge(this, data); + } + + /** + * Color of the non-completed pb + * + * @title Base Background Color + */ + baseBgColor?: string | undefined; + + /** + * Color of the completed pb + * + * @title Background Color + */ + bgColor?: string | undefined; + + /** + * Toggles whether to show progress bar + * + * @title Show Progress Bar + * @default false + */ + hide?: boolean | undefined; + + /** + * Vertical orientation of the progress bar + * + * @title Position + * @default 'bottom' + */ + position?: TVerticalPositionCore | undefined; + + /** + * Component type + * + * Can be one of two types: + * (1) The USWDS Step Indicator @see https://trussworks.github.io/react-uswds/?path=/docs/components-step-indicator + * (2) React progress bar @see https://katerinalupacheva.github.io/react-progress-bar/ + * + * @title Type + * @default 'progress-bar' + */ + type?: TProgressBarStatusType; + + visible?: boolean | undefined; +} diff --git a/packages/questionable-core/src/composable/config/QuestionConfig.ts b/packages/questionable-core/src/composable/config/QuestionConfig.ts new file mode 100644 index 00000000..cfc9b0e2 --- /dev/null +++ b/packages/questionable-core/src/composable/config/QuestionConfig.ts @@ -0,0 +1,38 @@ +import { merge } from 'lodash'; +import { BaseCore } from '../BaseCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; +/** + * Configuration for question display + */ +export class QuestionConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof QuestionConfigCore) { + return data; + } + return new QuestionConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + merge(this, data); + } + + /** + * Determines whether to show border on radios and checkboxes + * + * @title Show Answer Border + * @default true + */ + showAnswerBorder?: boolean | undefined; + + visible?: boolean | undefined; +} diff --git a/packages/questionable-core/src/composable/config/StepConfig.ts b/packages/questionable-core/src/composable/config/StepConfig.ts new file mode 100644 index 00000000..1a2fc63f --- /dev/null +++ b/packages/questionable-core/src/composable/config/StepConfig.ts @@ -0,0 +1,55 @@ +import { merge } from 'lodash'; +import { BaseCore } from '../BaseCore'; +import { ClassList, TInstanceOf, checkInstanceOf } from '../../lib/instanceOf'; + +/** + * Customizations for styling and formatting of the steps + */ +export class StepConfigCore extends BaseCore { + public get instanceOfCheck(): TInstanceOf { + return ClassList.config; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static [Symbol.hasInstance](obj: any) { + return checkInstanceOf({ names: [ClassList.config], obj }); + } + + public static override create(data: Partial = {}) { + if (data instanceof StepConfigCore) { + return data; + } + return new StepConfigCore(data); + } + + constructor(data: Partial = {}) { + super(data); + merge(this, data); + } + + /** + * Class determines whether cards have borders + * + * @title Border Class + * @default 'border-0' + */ + borderClass?: 'border-ink' | 'border-0'; + + /** + * Toggles whether steps' ids are shown next to the question text + * + * @title Show Step Id + * @default false + */ + showStepId?: boolean | undefined; + + /** + * Class to apply to title. Use to add background to question text + * + * @title Title Class + * @default '' + */ + titleClass?: 'bg-base-lightest' | ''; + + visible?: boolean | undefined; +} diff --git a/packages/questionable-core/src/composable/config/_exports.ts b/packages/questionable-core/src/composable/config/_exports.ts new file mode 100644 index 00000000..7bceb691 --- /dev/null +++ b/packages/questionable-core/src/composable/config/_exports.ts @@ -0,0 +1,6 @@ +export * from './ButtonConfig'; +export * from './NavigationConfig'; +export * from './PagesConfig'; +export * from './ProgressBarConfig'; +export * from './QuestionConfig'; +export * from './StepConfig'; diff --git a/packages/questionable-core/src/composable/index.ts b/packages/questionable-core/src/composable/index.ts deleted file mode 100644 index 159a3b3a..00000000 --- a/packages/questionable-core/src/composable/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './FormCore'; -export * from './QuestionableConfigCore'; -export * from './EventEmitterCore'; -export * from './QuestionnaireCore'; diff --git a/packages/questionable-core/src/constructable/Factory.ts b/packages/questionable-core/src/constructable/Factory.ts new file mode 100644 index 00000000..ac7bdc09 --- /dev/null +++ b/packages/questionable-core/src/constructable/Factory.ts @@ -0,0 +1,237 @@ +import { isEmpty, merge } from 'lodash'; +import { + ActionCore as Action, + AnswerCore as Answer, + BaseCore as Base, + BranchCore as Branch, + ButtonCore, + create, + PageCore as Page, + PagesCore as Pages, + QuestionableConfigCore as Config, + QuestionCore as Question, + RefCore as Ref, + RequirementCore as Requirement, + ResponseCore as Response, + ResultCore as Result, + SectionCore as Section, + TCtor, +} from '../composable/_exports'; +import { ACTION_TYPE } from '../metadata/properties/type/TActionType'; +import { + ClassList, EClassList, +} from '../lib/instanceOf'; + +export abstract class Factory { + public static addActions(data: Partial[]) { + return data.map((d) => Factory._addAction(d)); + } + + private static _addAction(inp: Partial): Action { + if (inp instanceof Action) { + return inp; + } + const data = merge( + { + label: inp.title, + type: ACTION_TYPE.NONE, + }, + inp, + ); + return create(Action, data); + } + + public static addAnswers(data: Partial[]) { + return data.map((d) => Factory._addAnswer(d)); + } + + private static _addAnswer(data: Partial): Answer { + return (data instanceof Answer) ? data : new Answer(data); + } + + public static addPages(data: Partial[]) { + return data.map((d) => Factory._addPage(d)); + } + + private static _addPage(data: Partial): Page { + return (data instanceof Page) ? data : new Page(data); + } + + public static addBranches(data: Partial[]) { + return data.map((d) => Factory._addBranch(d)); + } + + private static _addBranch(data: Partial): Branch { + return (data instanceof Branch) ? data : new Branch(data); + } + + public static addQuestions(data: Partial[]) { + return data.map((d) => Factory._addQuestion(d)); + } + + private static _addQuestion(data: Partial): Question { + return (data instanceof Question) ? data : new Question(data); + } + + public static addRefs(data: Partial[]) { + return data.map((d) => Factory._addRef(d)); + } + + private static _addRef(data: Partial): Ref { + return (data instanceof Ref) ? data : new Ref(data); + } + + public static addResults(data: Partial[]) { + return data.map((d) => Factory._addResult(d)); + } + + private static _addResult(data: Partial): Result { + return (data instanceof Result) ? data : new Result(data); + } + + public static addRequirements(data: Partial[]) { + return data.map((d) => Factory._addRequirement(d)); + } + + private static _addRequirement(data: Partial): Requirement { + return (data instanceof Requirement) ? data : new Requirement(data); + } + + public static addResponses(data: Partial[]) { + return data.map((d) => Factory._addResponse(d)); + } + + private static _addResponse(data: Partial): Response { + return (data instanceof Response) ? data : new Response(data); + } + + public static addSections(data: Partial
[]) { + return data.map((d) => Factory._addSection(d)); + } + + private static _addSection(data: Partial
): Section { + return (data instanceof Section) ? data : new Section(data); + } + + public static add(c: TCtor, inp: Partial[], out: Partial[] = []) { + return inp.map((ic) => Factory.addOne(c, ic, out)); + } + + public static addOne(Ctor: TCtor, inp: Partial, out: Partial[] = []): T { + const ret = Factory._addOne(Ctor, inp); + if (out) { + out.push(ret); + } + return ret; + } + + private static _addOne(Ctor: TCtor, inp: Partial): T { // eslint-disable-line sonarjs/cognitive-complexity + const nu = new Ctor(inp); + if (nu instanceof Action || nu.instanceOfCheck === ClassList.action) { + Factory._addAction(nu); + } else if ( + nu instanceof Section + || nu.instanceOfCheck === ClassList.section + ) { + Factory._addSection(nu); + } else if ( + nu instanceof Question + || nu.instanceOfCheck === ClassList.question + ) { + Factory._addQuestion(nu); + } else if (nu instanceof Page || nu.instanceOfCheck === ClassList.page) { + Factory._addPage(nu); + } else if ( + nu instanceof Branch + || nu.instanceOfCheck === ClassList.branch + ) { + Factory._addBranch(nu); + } else if ( + nu instanceof Result + || nu.instanceOfCheck === ClassList.result + ) { + Factory._addResult(nu); + } else if (nu instanceof Config) { + // Nothing yet + } else if ( + nu instanceof Response + || nu.instanceOfCheck === ClassList.response + ) { + Factory._addResponse(nu); + } else if ( + nu instanceof Requirement + || nu.instanceOfCheck === ClassList.requirement + ) { + Factory._addRequirement(nu); + } else if (nu instanceof Pages) { + // Nothing yet + } else if ( + nu instanceof Answer + || nu.instanceOfCheck === ClassList.answer + ) { + Factory._addAnswer(nu); + } else if (nu instanceof Ref || nu.instanceOfCheck === ClassList.ref) { + Factory._addRef(nu); + } + return nu; + } + + public static classCreate(c: T, data: Partial): Action; + + public static classCreate + (c: T, data: Partial | undefined, optional: boolean): Action | undefined; + + public static classCreate(c: T, data: Partial): Answer; + + public static classCreate(c: T, data: Partial): Branch; + + public static classCreate(c: T, + data: Partial | undefined, optional: boolean): Branch | undefined; + + public static classCreate(c: T, data: Partial): ButtonCore; + + public static classCreate(c: T, data: Partial): Page; + + public static classCreate(c: T, data: Partial): Question; + + public static classCreate + (c: T, data: Partial | undefined, optional: boolean): Question | undefined; + + public static classCreate(c: T, data: Partial): Requirement; + + public static classCreate + (c: T, data: Partial| undefined, optional: boolean): Requirement|undefined; + + public static classCreate(c: T, data: Partial): Response; + + public static classCreate(c: T, data: Partial): Result; + + public static classCreate(c: T, data: Partial
): Section; + + public static classCreate + (c: T, data: Partial
| undefined, optional: boolean): Section | undefined; + + public static classCreate(c: T, data: unknown, optional = false) { + if (optional && isEmpty(data)) { + return undefined; + } + switch (c) { + case EClassList.ACTION: + return Factory._addAction(data as Partial) as Action; + case EClassList.ANSWER: + return Factory._addAnswer(data as Partial) as Answer; + case EClassList.BRANCH: + return Factory._addBranch(data as Partial) as Branch; + case EClassList.QUESTION: + return Factory._addQuestion(data as Partial) as Question; + case EClassList.SECTION: + return Factory._addSection(data as Partial
) as Section; + default: + return Factory._addRef(data as Partial) as Ref; + } + } +} + +export const { + classCreate, +} = Factory; diff --git a/packages/questionable-react-component/src/composable/Questionnaire.ts b/packages/questionable-core/src/constructable/GateLogicCore.ts similarity index 56% rename from packages/questionable-react-component/src/composable/Questionnaire.ts rename to packages/questionable-core/src/constructable/GateLogicCore.ts index 310a279d..6f024b96 100644 --- a/packages/questionable-react-component/src/composable/Questionnaire.ts +++ b/packages/questionable-core/src/constructable/GateLogicCore.ts @@ -1,81 +1,158 @@ -import { ArrayUnique } from 'class-validator'; -import { groupBy, isEmpty, merge } from 'lodash'; -import { - log, - QuestionnaireCore, - matches, - TAgeCore, - TAgeCalcCore, - ACTION, - DIRECTION, - isEnum, - MODE, - PAGE_TYPE, - PROGRESS_BAR_STATUS, - QUESTION_TYPE, - STEP_TYPE, -} from '@usds.gov/questionable-core'; -import { IAction } from '../survey/IAction'; -import { IBranch } from '../survey/IBranch'; -import { IForm } from '../survey/IForm'; -import { IPageConfig } from '../survey/IQuestionableConfig'; -import { IPages } from '../survey/IPages'; -import { IQuestionnaire } from '../survey/IQuestionnaire'; -import { IResult } from '../survey/IResult'; -import { IStepData } from '../survey/IStepData'; -import { QuestionableConfig } from './QuestionableConfig'; -import { setBranch } from '../state/persists'; -import { - IPage, - IQuestion, - IRequirement, - IResponse, - ISection, - IStep, -} from '../survey/IStep'; - -/** - * Utility wrapper for survey state - */ -export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { - @ArrayUnique((action: IAction) => action.id) - public actions: IAction[] = []; - - public branches: IBranch[] = []; - - public config: QuestionableConfig = new QuestionableConfig(); - - public flow: string[] = []; - - public header = ''; - - @ArrayUnique((result: IResult) => result.label) - public results: IResult[] = []; - - public pages!: IPages; - - @ArrayUnique((question: IQuestion) => question.id) - public questions: IQuestion[] = []; - - @ArrayUnique((section: ISection) => section.id) - public sections: ISection[] = []; - - protected steps: IStep[] = []; - - constructor(data: Partial) { - super(data); - merge(this, data); - - // Create a new collection for our flow logic - this.steps = this.questions.map((q, i) => ({ - order: i, - ...q, - })); - - this.init(); - - // Wizard flow is defined as linear sequence of unique ids - this.flow = this.steps.map((q) => q.id); +import { groupBy, isEmpty, noop } from 'lodash'; +import { ResultCore } from '../composable/ResultCore'; +import { FormCore } from '../composable/FormCore'; +import { PageCore } from '../composable/PageCore'; +import { QuestionnaireCore } from '../composable/QuestionnaireCore'; +import { StepCore } from '../composable/StepCore'; +import { DIRECTION, isEnum, MODE } from '../lib/enums'; +import { log, toggleOut } from '../lib/logger'; +import { matches } from '../lib/helpers'; +import { TAgeCalcCore } from '../lib/types'; +import { TAgeCore } from '../metadata/types/TAgeCore'; +import { ActionCore } from '../composable/ActionCore'; +import { isValid } from './lib/isValid'; +import { PagesConfigCore } from '../composable/config/PagesConfig'; +import { QuestionCore } from '../composable/QuestionCore'; +import { SectionCore } from '../composable/SectionCore'; +import { BranchCore } from '../composable/BranchCore'; +import { RequirementCore } from '../composable/RequirementCore'; +import { ResponseCore } from '../composable/ResponseCore'; +import { STEP_TYPE } from '../metadata/properties/type/TStepType'; +import { QUESTION_TYPE } from '../metadata/properties/type/TQuestionType'; +import { PAGE_TYPE, TPageType } from '../metadata/properties/type/TPageType'; +import { PROGRESS_BAR_STATUS } from '../metadata/types/TProgressBarStatusType'; +import { ACTION_TYPE, TActionType } from '../metadata/properties/type/TActionType'; + +type TPageSet = + | { + config?: Partial; + data?: PageCore; + } + | undefined; + +export class GateLogicCore { + #form!: FormCore; + + #questionnaire!: QuestionnaireCore; + + constructor(questionnaire: QuestionnaireCore, form: FormCore) { + this.init(questionnaire, form); + } + + init(questionnaire: QuestionnaireCore, form: FormCore) { + this.#questionnaire = questionnaire; + this.#form = form; + // Performs constructor validation on the survery inputs. + this.validateInput(); + this.syncBranches(); + this.setPageDefaults(); + } + + public get questionnaire() { + return this.#questionnaire; + } + + public get form() { + return this.#form; + } + + public get flow() { + return this.questionnaire.flow; + } + + public get config() { + return this.questionnaire.config; + } + + public get steps() { + return this.questionnaire.steps; + } + + public get pages() { + return this.questionnaire.pages; + } + + public get pageList() { + return this.pages.all(); + } + + public enableLog() { + if (this.config.dev) { + // Fork for future enhancements to dev logging + GateLogicCore.enableLogging(); + } + return GateLogicCore.enableLogging(); + } + + public static enableLogging() { + toggleOut('on'); + } + + public goToStep(step: StepCore, cb = noop): void { + if (cb) { + cb(step, this); + } + } + + public goToNextStep(s: StepCore): void { + const step = this.getNextStep(s); + const dir = DIRECTION.FORWARD; + this.config.events?.page({ + dir, + step, + }); + this.goToStep(step); + } + + public goToPrevStep(s: StepCore): void { + const step = this.getPreviousStep(s); + const dir = DIRECTION.BACKWARD; + this.config.events?.page({ dir, step }); + this.goToStep(step); + } + + /** + * Determines whether the user should be allowed to continue + * @param props + * @returns + */ + public isNextEnabled(s: StepCore): boolean { + if (!s.id) { + throw new Error('This survery is not defined'); + } + if (s.type === STEP_TYPE.LANDING) { + return true; + } + if (s.type === STEP_TYPE.SUMMARY) { + return true; + } + // KLUDGE Alert: this is not an elegant way to solve this + if (s.type === QUESTION_TYPE.DOB) { + const yearsOld = this.form.age?.years || 0; + return yearsOld > 0; + } + if (!this.form) { + return false; + } + return isValid({ form: this.form, step: s }); + } + + /** + * Fetches the first step + * @returns + */ + getFirstStep(): T { + const ret = this.steps[0] as T; + if (!ret) { + this.throw('There is no step'); + } + // if (isEnum(QUESTION_TYPE, ret.type) && (ret instanceof T)) { + // return ret as QuestionCore; + // } + // if (isEnum(PAGE_TYPE, ret.type)) { + // return ret as IPageCore; + // } + return ret; } /** @@ -83,7 +160,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param id unique identifier of the question * @returns */ - getStepById(id: string): IStep { + getStepById(id: string): StepCore { const ret = this.steps.find((q) => q.id === id); if (!ret) { this.throw(`Step id: ${id} not found in survery`); @@ -96,12 +173,12 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param id unique identifier of the question * @returns */ - getPageById(id: string): IPage { + getPageById(id: string): PageCore { const ret = this.getStepById(id); - if (!isEnum(PAGE_TYPE, ret.type)) { + if (!isEnum({ enm: PAGE_TYPE, value: ret.type })) { this.throw(`Step id: ${id} is not a page`); } - return ret as IPage; + return ret as PageCore; } /** @@ -109,7 +186,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param id unique identifier of the question * @returns */ - getQuestion(q: Partial): IQuestion { + getQuestion(q: { id: string }): QuestionCore { if (!q.id) { this.throw(`Question ${q} is not defined`); } @@ -121,54 +198,67 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param id unique identifier of the question * @returns */ - getQuestionById(id: string): IQuestion { + getQuestionById(id: string): QuestionCore { const ret = this.getStepById(id); - if (!isEnum(QUESTION_TYPE, ret.type)) { + if (!isEnum({ enm: QUESTION_TYPE, value: ret.type })) { this.throw(`Step id: ${id} not a question`); } - return ret as IQuestion; + return ret as QuestionCore; } + // protected isValidExit(question: QuestionCore, form: FormCore, skip = 0) { + // let allowExit = true; + // if (skip === 0 + // && direction === DIRECTION.FORWARD + // && thisQuestion.exitRequirements + // && thisQuestion.exitRequirements.length > 0) { + // allowExit = thisQuestion.exitRequirements.every((r) => + // this.meetsAllRequirements(r, form)); + // } + // return allowExit; + // } + /** * Returns the next step in the sequence which is permitted by the current state of the form */ // eslint-disable-next-line sonarjs/cognitive-complexity - getStep(thisStep: string, form: IForm, direction: DIRECTION, skip = 0): string { - const nextStep = this.flow.indexOf(thisStep) !== -1 - ? this.flow[this.flow.indexOf(thisStep) + direction] + getStep(data: StepCore, direction: DIRECTION, skip = 0): StepCore { + const thisQuestion = data; + const nextStepId = this.flow.indexOf(thisQuestion.id) !== -1 + ? this.flow[this.flow.indexOf(thisQuestion.id) + direction] : undefined; // If there are no more steps, stay on current - if (!nextStep) { - return thisStep; + if (!nextStepId) { + return thisQuestion; } - const thisQuestion = this.getStepById(thisStep); - if (skip === 0 + if ( + skip === 0 && direction === DIRECTION.FORWARD - && thisQuestion.exitRequirements - && thisQuestion.exitRequirements.length > 0) { + && !isEmpty(thisQuestion.exitRequirements) + ) { const allowExit = thisQuestion.exitRequirements.every((r) => - this.meetsAllRequirements(r, form)); + this.meetsAllRequirements(r)); if (!allowExit) { - return thisStep; + return thisQuestion; } } if (this.config.mode === MODE.EDIT) { - return nextStep; + return this.getStepById(nextStepId); } // Special handling for results - const hasResults = this.getResults(form).length > 0; - if (nextStep === STEP_TYPE.RESULTS && !hasResults) { - return STEP_TYPE.NO_RESULTS; + const hasResults = this.getResults().length > 0; + if (nextStepId === STEP_TYPE.RESULTS && !hasResults) { + return this.pages.noResultsPage; } - if (nextStep === STEP_TYPE.NO_RESULTS && hasResults) { - return STEP_TYPE.RESULTS; + if (nextStepId === STEP_TYPE.NO_RESULTS && hasResults) { + return this.pages.resultsPage; } - const nextQuestion = this.getStepById(nextStep); - if (!nextQuestion?.entryRequirements) { - return nextStep; + const nextQuestion = this.getStepById(nextStepId); + if (isEmpty(nextQuestion?.entryRequirements)) { + return nextQuestion; } // match is a tri-state (undefined === unset) @@ -177,7 +267,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { // Each requirement is joined by `OR` nextQuestion.entryRequirements?.forEach((r) => { // This safely handles cases where requirement parameters are undefined - const next = this.meetsAllRequirements(r, form); + const next = this.meetsAllRequirements(r); if (match === undefined) { match = next; @@ -188,14 +278,14 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { // If the requested step meets all requirements, return it if (match) { - return nextStep; + return nextQuestion; } // Get the next step whose requirements are met - const n = this.getStep(nextStep, form, direction, skip + 1); - if (n !== nextStep) { + const n = this.getStep(nextQuestion, direction, skip + 1); + if (n !== nextQuestion) { return n; } - return thisStep; + return thisQuestion; } /** @@ -203,10 +293,9 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props step context * @returns step id */ - getNextStep(props: IStepData): string { - const thisStep = props.stepId as string; - const dir = DIRECTION.FORWARD; - return this.getStep(thisStep, props.form, dir); + getNextStep(props: StepCore): StepCore { + const dir = DIRECTION.FORWARD; + return this.getStep(props, dir); } /** @@ -214,10 +303,9 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props step context * @returns step id */ - getPreviousStep(props: IStepData): string { - const thisStep = props.stepId as string; - const dir = DIRECTION.BACKWARD; - return this.getStep(thisStep, props.form, dir); + getPreviousStep(props: StepCore): StepCore { + const dir = DIRECTION.BACKWARD; + return this.getStep(props, dir); } /** @@ -225,9 +313,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props * @returns */ - getProgressPercent(props: IStepData): number { - const stepId = `${props.stepId}`; - const step = this.getStepById(stepId); + getProgressPercent(step: StepCore): number { if (matches(step.type, PAGE_TYPE.LANDING)) { // Landing page exists before progress starts // less than 0% progress can be interpretted as 'do not display' @@ -247,14 +333,14 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { } // if we have branches then we need to do this, otherwise we need to get the number of steps - const answerable = this.getAllAnswerableQuestions(props); + const answerable = this.getAllAnswerableQuestions(step); const lastStep = answerable.length; // sections[sections.length - 1]?.lastStep; // if there is no step, the questionnaire has just started if (lastStep <= 0) { return 0.1; } - const thisStepIdx = answerable.indexOf(stepId) + 1; + const thisStepIdx = answerable.indexOf(step.id) + 1; // add 2 to account for the summary and result steps let lastStepIdx = lastStep + 2; if (this.config.mode === MODE.EDIT) { @@ -272,15 +358,13 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props * @returns string[] */ - getAllAnswerableQuestions(props: IStepData): string[] { - const stepId = `${props.stepId}`; - const step = this.getStepById(stepId); - if (!isEnum(QUESTION_TYPE, step.type)) { + getAllAnswerableQuestions(step: StepCore): string[] { + if (!isEnum({ enm: QUESTION_TYPE, value: step.type })) { return []; } - if (this.branches.length) { - const question = step as IQuestion; + if (this.questionnaire.branches.length) { + const question = step as QuestionCore; return this.getBranchQuestions(question); } return this.getQuestionsWithoutBranches(); @@ -291,14 +375,19 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param step * @returns string[] */ - protected getBranchQuestions(step: IQuestion): string[] { - const question = step as IQuestion; + protected getBranchQuestions(step: StepCore): string[] { + const question = step as QuestionCore; if (question.branch) { - setBranch(this.header, `${question.branch.title}`); + this.config.events.gate({ + data: { + [this.questionnaire.header]: `${question.branch.title}`, + }, + gate: 'branch', + }); return ( - this.branches + this.questionnaire.branches .find((b) => b.id === question.branch?.id) ?.questions.map((q) => q.id) || [] ); @@ -312,7 +401,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { */ protected getQuestionsWithoutBranches(): string[] { return this.steps - .filter((q) => isEnum(QUESTION_TYPE, q.type)) + .filter((q) => isEnum({ enm: QUESTION_TYPE, value: q.type })) .map((q) => q.id); } @@ -321,14 +410,13 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props * @returns */ - getAnswerableQuestions(props: IStepData): string[] { - return this.questions + getAnswerableQuestions(): string[] { + return this.questionnaire.questions .filter( (q) => !q.entryRequirements || q.entryRequirements.length === 0 - || q.entryRequirements.some((r) => - this.meetsAllRequirements(r, props.form, true)), + || q.entryRequirements.some((r) => this.meetsAllRequirements(r, true)), ) .map((q) => q.id); } @@ -338,39 +426,41 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param props * @returns */ - getSections(props: IStepData): ISection[] { - if (!props || !this.sections || this.sections.length === 0) { + getSections(thisQuestion: StepCore): SectionCore[] { + if ( + !thisQuestion + || !this.questionnaire.sections + || this.questionnaire.sections.length === 0 + ) { return []; } - const thisStep = props.stepId as string; - const thisQuestion = this.getStepById(thisStep); const thisQuestionIdx = this.steps.indexOf(thisQuestion); // Get all sections that meet the requirements based on current answers - let sections = this.sections.filter( + let sections = this.questionnaire.sections.filter( (s) => s.requirements.length === 0 - || s.requirements.some((r) => this.meetsAllRequirements(r, props.form)), + || s.requirements.some((r) => this.meetsAllRequirements(r)), ); if (this.config.mode === MODE.EDIT) { // In design mode, all sections are valid - sections = [...this.sections]; + sections = [...this.questionnaire.sections]; } return sections.map((s) => { - const section = { ...s }; - section.lastStep = this.questions.reduce( - (acc, q, index) => (q.section.id === s.id ? index : acc), + const section = SectionCore.create(s); + section.lastStep = this.questionnaire.questions.reduce( + (acc, q, index) => (q.section?.id === s.id ? index : acc), -1, ); if (matches(section.id, PAGE_TYPE.RESULTS)) { - section.lastStep = this.questions.length - 2; + section.lastStep = this.questionnaire.questions.length - 2; } else if (matches(section.id, PAGE_TYPE.LANDING)) { section.lastStep = 0; } if (section.lastStep < 0) { section.status = PROGRESS_BAR_STATUS.INCOMPLETE; - } else if (matches(section.id, thisQuestion.section.id)) { + } else if (matches(section.id, thisQuestion.section?.id)) { section.status = PROGRESS_BAR_STATUS.CURRENT; } else if (section.lastStep < thisQuestionIdx) { section.status = PROGRESS_BAR_STATUS.COMPLETE; @@ -384,10 +474,10 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param form * @returns */ - getResults(form: IForm): IResult[] { - return this.results.filter((r) => + getResults(): ResultCore[] { + return this.questionnaire.results.filter((r) => r.requirements.some((match) => { - if (this.meetsAllRequirements(match, form)) { + if (this.meetsAllRequirements(match)) { Object.assign(r, { match }); return true; } @@ -399,8 +489,8 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * Gets the appropriate action given a set of results * @returns */ - getActionByType(type: ACTION): IAction { - const action = this.actions.find((a) => a.type === type); + getActionByType(type: TActionType): ActionCore { + const action = this.questionnaire.actions.find((a) => a.type === type); if (!action) { this.throw(`No matching action found for ${type}`); } @@ -411,14 +501,16 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * Gets the appropriate action given a set of results * @returns */ - getAction(results: IResult[]): IAction { + getAction(results: ResultCore[]): ActionCore { const groupedByAction = groupBy(results, 'action.id'); - const hybrid = this.actions.find((a) => a.type === ACTION.HYBRID); + const hybrid = this.questionnaire.actions.find( + (a) => a.type === ACTION_TYPE.HYBRID, + ); // If group above has more than one type of action, the resolved action will be a hybrid let match = hybrid; const actions = Object.keys(groupedByAction); if (actions.length === 1) { - match = this.actions.find((a) => a.id === actions[0]); + match = this.questionnaire.actions.find((a) => a.id === actions[0]); if (!match) { this.throw(`Action id ${actions[0]} could not be found.`); } @@ -443,13 +535,13 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * Ensure the survey is constructed with (minimally) valid data */ protected validateInput() { - if (this.questions?.length <= 0) { + if (this.questionnaire.questions?.length <= 0) { this.throw('No questions have been defined.'); } - if (this.header?.length <= 0) { - this.throw('No header has been defined.'); - } - if (this.results?.length <= 0) { + // if (this.questionnaire.header?.length <= 0) { + // this.throw('No header has been defined.'); + // } + if (this.questionnaire.results?.length <= 0) { this.throw('No results have been defined.'); } } @@ -462,28 +554,31 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { */ protected syncBranches(): void { // Branches defined take priority; sync these first - this.branches.forEach((b) => { + this.questionnaire.branches.forEach((b) => { b.questions.forEach((bq) => { - const question = this.questions.find((q) => q.id === bq.id); + const question = this.questionnaire.questions.find( + (q) => q.id === bq.id, + ); if (question && question?.branch?.id !== b.id) { question.branch = b; } }); }); - this.questions.forEach((q) => { + this.questionnaire.questions.forEach((q) => { // Branches are optional on questions if (!q.branch?.id) { return; } - const exists = this.branches.find((b) => b.id === q.branch?.id); - const validateBranch = exists || (q.branch as IBranch); + const exists = this.questionnaire.branches.find( + (b) => b.existsIn(q) || q.branch === b, + ); + const validateBranch = exists || (q.branch as BranchCore); if (!exists) { - this.branches.push(validateBranch); + this.questionnaire.add(validateBranch); } - validateBranch.questions = validateBranch.questions || []; if (!validateBranch.questions.find((bq) => bq.id === q.id)) { - validateBranch.questions.push(q); + validateBranch.add(q); } }); } @@ -493,36 +588,18 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param type Page Type * @returns */ - protected getPageSet = ( - type: PAGE_TYPE, - ): { - config?: Partial; - data?: IPage; - } => { - switch (type) { - case PAGE_TYPE.LANDING: - return { - config: this.config.pages.landing, - data: this.pages.landingPage, - }; - case PAGE_TYPE.NO_RESULTS: - return { - config: this.config.pages.noresults, - data: this.pages.noResultsPage, - }; - case PAGE_TYPE.RESULTS: - return { - config: this.config.pages.results, - data: this.pages.resultsPage, - }; - case PAGE_TYPE.SUMMARY: - return { - config: this.config.pages.summary, - data: this.pages.summaryPage, - }; - default: - return this.throw(`No data for page type ${type}`); + // eslint-disable-next-line class-methods-use-this + protected getPageSet = (type: TPageType): TPageSet => { + const data = this.pageList.find( + (p: PageCore | undefined) => p?.type === type, + ); + if (data) { + return { + config: {}, + data, + }; } + return undefined; }; /** @@ -530,10 +607,11 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param idx index of the page in `this.steps` * @param type LANDING, RESULTS, etc */ - protected setPage(idx: number, type: PAGE_TYPE): void { + protected setPage(idx: number, type: TPageType): void { const error = 'step is not correctly defined or defined more than once'; const page = this.getPageSet(type); + if (!page) return; // visible is truthy unless explicitly set to false if (!page.data || page?.config?.visible === false) { if (this.steps.filter((q) => q.type === type).length > 0) { @@ -570,18 +648,8 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { this.setPage(this.steps.length - 1, PAGE_TYPE.NO_RESULTS); } - /** - * Performs constructor validation on the survery inputs. - */ - protected init(): void { - this.validateInput(); - this.syncBranches(); - this.setPageDefaults(); - } - public meetsAllRequirements( - requirement: IRequirement, - form: IForm, + requirement: RequirementCore, allowUnanswered = false, ) { const { @@ -590,9 +658,9 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { // Internal to each requirement, all evaluations are `AND` // This safely handles cases where requirement parameters are undefined return ( - Questionnaire.meetsMinAgeRequirements(form, minAge) - && Questionnaire.meetsMaxAgeRequirements(form, maxAge) - && Questionnaire.meetsAgeCalcRequirements(form, ageCalc) + GateLogicCore.meetsMinAgeRequirements(this.form, minAge) + && GateLogicCore.meetsMaxAgeRequirements(this.form, maxAge) + && GateLogicCore.meetsAgeCalcRequirements(this.form, ageCalc) && this.meetsAnswerRequirements(answers, allowUnanswered) ); } @@ -603,7 +671,10 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param minAge a TAge object or undefined * @returns true if no min age, else true if age is >= min age */ - public static meetsMinAgeRequirements(form: IForm, minAge?: TAgeCore): boolean { + public static meetsMinAgeRequirements( + form: FormCore, + minAge?: TAgeCore, + ): boolean { if (!minAge) return true; if (form.age === undefined) { @@ -625,7 +696,10 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @param maxAge a TAge object or undefined * @returns true if no max age, else true if age is <= max age */ - public static meetsMaxAgeRequirements(form: IForm, maxAge?: TAgeCore): boolean { + public static meetsMaxAgeRequirements( + form: FormCore, + maxAge?: TAgeCore, + ): boolean { if (!maxAge) return true; if (form.age === undefined) { return false; @@ -647,7 +721,7 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @returns */ protected static meetsAgeCalcRequirements( - form: IForm, + form: FormCore, ageCalc?: TAgeCalcCore, ): boolean { if (!ageCalc) return true; @@ -699,12 +773,13 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { * @returns true if all answers are valid or if no answers are required */ protected meetsAnswerRequirements( - answers?: IResponse[], + answers?: ResponseCore[], allowUnanswered = false, ): boolean { if (!answers || answers.length <= 0) return true; return answers.every((a) => { + if (!a.question) return true; const question = this.getQuestion(a.question); if (question.answers?.length > 0) { // Allowed answers are an array. Any matched answer makes the response valid. @@ -727,4 +802,18 @@ export class Questionnaire extends QuestionnaireCore implements IQuestionnaire { return true; }); } + + getStepType(step: StepCore) { + if (isEnum({ enm: QUESTION_TYPE, value: step.type })) { + return 'question'; + } + if (isEnum({ enm: PAGE_TYPE, value: step.type })) { + return 'page'; + } + return 'unknown'; + } + + isComplete(step: StepCore) { + return this.getProgressPercent(step) === 100; + } } diff --git a/packages/questionable-core/src/constructable/Questioner.ts b/packages/questionable-core/src/constructable/Questioner.ts new file mode 100644 index 00000000..ff6c23ce --- /dev/null +++ b/packages/questionable-core/src/constructable/Questioner.ts @@ -0,0 +1,63 @@ +import { FormCore } from '../composable/FormCore'; +import { QuestionCore } from '../composable/QuestionCore'; +import { TDateOfBirthCore } from '../metadata/types/TAgeCore'; +import { TQForm } from './lib/TQForm'; +import { isValid } from './lib/isValid'; +import { isSelected } from './lib/isSelected'; +import { toString } from './lib/toString'; +import { getBirthdate } from './lib/getBirthdate'; +import { updateForm } from './lib/updateForm'; +import { toBirthdate } from './lib/toBirthdate'; + +export class Questioner { + #question: QuestionCore; + + #form: FormCore; + + constructor({ question, form }: TQForm) { + this.#question = question; + this.#form = form; + } + + public updateForm({ answer }: {answer: string}): void { + return updateForm({ answer, form: this.#form, question: this.#question }); + } + + /** + * Determines if the answer is valid and selected + * @param answer + * @param props + * @returns + */ + public isSelected({ answer }: {answer: string}) { + return isSelected({ answer, form: this.#form, question: this.#question }); + } + + public toString() { + return toString({ question: this.#question }); + } + + /** + * Gets a birthdate's DateTime from a form + * @param props + * @returns + */ + public getBirthdate() { + return getBirthdate({ form: this.#form, question: this.#question }); + } + + /** + * Converts a Date of Birth type into a string + * @param dob + * @returns + */ + public toBirthdate( + dob: TDateOfBirthCore, + ): string | undefined { + return toBirthdate({ dob, question: this.#question }); + } + + public isValid(): boolean { + return isValid({ form: this.#form, step: this.#question }); + } +} diff --git a/packages/questionable-core/src/constructable/Questionnaire.ts b/packages/questionable-core/src/constructable/Questionnaire.ts new file mode 100644 index 00000000..130f58be --- /dev/null +++ b/packages/questionable-core/src/constructable/Questionnaire.ts @@ -0,0 +1,82 @@ +/* eslint-disable import/no-cycle */ +/* eslint-disable no-useless-constructor */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable max-classes-per-file */ +import { ActionCore } from '../composable/ActionCore'; +import { StepCore } from '../composable/StepCore'; +import { QuestionableConfigCore } from '../composable/ConfigCore'; +import { ResultCore } from '../composable/ResultCore'; +import { PageCore } from '../composable/PageCore'; +import { QuestionnaireCore } from '../composable/QuestionnaireCore'; +import { QuestionCore } from '../composable/QuestionCore'; +import { BranchCore } from '../composable/BranchCore'; +import { SectionCore } from '../composable/SectionCore'; +import { TCollectable } from '../metadata/types/TCollectable'; +import { addToPool } from './lib/pools'; + +/** + * Utility wrapper for survey state + */ +export class Questionnaire< + Q extends S & QuestionCore, + P extends S & PageCore, + S extends StepCore = StepCore, +> { + #pages!: P[]; + + #questions!: Q[]; + + #steps!: S[]; + + #questionnaire: QuestionnaireCore; + + constructor(data: QuestionnaireCore) { + this.#questionnaire = data; + this.#steps = this.#questionnaire.steps.map((q) => q as unknown as S); + this.#questions = this.#questionnaire.questions.map((q) => q as unknown as Q); + + // this.#pages = this.#questionnaire.pageList.map((q) => q as unknown as P); + } + + public get actions(): ActionCore[] { + return this.#questionnaire.actions; + } + + public get branches(): BranchCore[] { + return this.#questionnaire.branches; + } + + public get config(): QuestionableConfigCore { + return this.#questionnaire.config; + } + + public get flow(): string[] { + return this.#questionnaire.flow; + } + + public get questions(): Q[] { + return this.#questions; + } + + public get steps(): S[] { + return this.#steps; + } + + public get sections(): SectionCore[] { + return this.#questionnaire.sections; + } + + public get results(): ResultCore[] { + return this.#questionnaire.results; + } + + public get pages(): P[] { + return this.#pages; + } + + public add(data: TCollectable) { + addToPool(data, this.#questionnaire); + return this; + } +} diff --git a/packages/questionable-core/src/constructable/ReferentialIntegrity.ts b/packages/questionable-core/src/constructable/ReferentialIntegrity.ts new file mode 100644 index 00000000..5a54e6f9 --- /dev/null +++ b/packages/questionable-core/src/constructable/ReferentialIntegrity.ts @@ -0,0 +1,19 @@ +// import { BranchCore, QuestionnaireCore } from '../composable'; + +export class ReferentialIntegrity { + // #questionnaire: QuestionnaireCore; + // constructor(questionnaire: QuestionnaireCore) { + // this.#questionnaire = questionnaire; + // } + // // eslint-disable-next-line class-methods-use-this + // syncBranches(_branch: BranchCore) { + // // TODO finish this + // // if (branch.questions) { + // // this.questions = data.questions.map((q) => questionnaire.getQuestionById(q.id)); + // // } + // // this.questions.forEach((q) => { + // // // eslint-disable-next-line no-param-reassign + // // q.branch = this; + // // }); + // } +} diff --git a/packages/questionable-core/src/constructable/SurveyBuilderCore.ts b/packages/questionable-core/src/constructable/SurveyBuilderCore.ts new file mode 100644 index 00000000..2ae68ee0 --- /dev/null +++ b/packages/questionable-core/src/constructable/SurveyBuilderCore.ts @@ -0,0 +1,207 @@ +/* eslint-disable */ +import { ActionCore as Action } from '../composable/ActionCore'; +import { AnswerCore as Answer } from '../composable/AnswerCore'; +import { BaseCore as Base } from '../composable/BaseCore'; +import { BranchCore as Branch } from '../composable/BranchCore'; +import { QuestionCore as Question } from '../composable/QuestionCore'; +import { PageCore as Page } from '../composable/PageCore'; + import { PagesCore as Pages } from '../composable/PagesCore'; + import { QuestionableConfigCore as Config } from '../composable/ConfigCore'; + import { QuestionnaireCore as Questionnaire } from '../composable/QuestionnaireCore'; + import { RefCore as Ref } from '../composable/RefCore'; + import { RequirementCore as Requirement } from '../composable/RequirementCore'; + import { ResponseCore as Response } from '../composable/ResponseCore'; + import { ResultCore as Result } from '../composable/ResultCore'; + import { SectionCore as Section } from '../composable/SectionCore'; +import {TCtor} from '../composable/TCtor'; +import { Factory } from './Factory'; +import { ClassList } from '../lib/instanceOf'; +import { MODE } from '../lib/enums'; +import { merge } from '../lib/merge'; +import { ACTION_TYPE } from "../metadata/properties/type/TActionType"; + +type TBuilderDefaults = { + section?: Section; +}; + +export class SurveyBuilder { + #actions: Action[] = []; + #answers: Answer[] = []; + #branches: Branch[] = []; + #config: Config; + #defaults: TBuilderDefaults; + #page: Page[] = []; + #pages: Pages = new Pages(); + #questions: Question[] = []; + #refs: Ref[] = []; + #requirements: Requirement[] = []; + #responses: Response[] = []; + #results: Result[] = []; + #sections: Section[] = []; + + constructor(data: Partial = {}) { + this.#actions = data.actions || []; + this.#branches = data.branches || []; + this.#config = data.config || new Config({ mode: MODE.VIEW }); + this.#defaults = {}; + this.#pages = data.pages || new Pages(); + this.#questions = data.questions || []; + this.#refs = []; + this.#sections = data.sections || []; + } + + setDefaults(data: Section | Branch) { + if (data instanceof Section) { + return merge({ params: [this.#defaults.section, data] }); + } + } + + addActions(data: Partial[]) { + return data.map((d) => this.#addAction(d)); + } + #addAction(inp: Partial): Action { + const data = merge( + { + params: [{ + type: ACTION_TYPE.NONE, + label: inp.title, + }, + inp] + }, + ); + return Factory.addOne(Action, data,this.#actions); + } + + addAnswers(data: Partial[]) { + return data.map((d) => this.#addAnswer(d)); + } + #addAnswer(data: Partial): Answer { + return Factory.addOne(Answer, data, this.#answers); + } + + addPages(data: Partial[]) { + return data.map((d) => this.#addPage(d)); + } + #addPage(data: Partial): Page { + return Factory.addOne(Page, data, this.#page); + } + + addBranches(data: Partial[]) { + return data.map((d) => this.#addBranch(d)); + } + #addBranch(data: Partial): Branch { + return Factory.addOne(Branch, data, this.#branches); + } + + addQuestions(data: Partial[]) { + return data.map((d) => this.#addQuestion(d)); + } + #addQuestion(data: Partial): Question { + return Factory.addOne(Question, data, this.#questions); + } + + addRefs(data: Partial[]) { + return data.map((d) => this.#addRef(d)); + } + #addRef(data: Partial): Ref { + return Factory.addOne(Ref, data, this.#refs); + } + + addResults(data: Partial[]) { + return data.map((d) => this.#addResult(d)); + } + #addResult(data: Partial): Result { + return Factory.addOne(Result, data, this.#results); + } + + addRequirements(data: Partial[]) { + return data.map((d) => this.#addRequirement(d)); + } + #addRequirement(data: Partial): Requirement { + return Factory.addOne(Requirement,data, this.#requirements); + } + + addResponses(data: Partial[]) { + return data.map((d) => this.#addResponse(d)); + } + #addResponse(data: Partial): Response { + return Factory.addOne(Response,data, this.#responses); + } + + addSections(data: Partial
[]) { + return data.map((d) => this.#addSection(d)); + } + #addSection(data: Partial
): Section { + return Factory.addOne(Section, data, this.#sections); + } + + add(c: TCtor, inp: Partial[]) { + return inp.map((ic) => this.#addOne(c, ic)); + } + + #addOne(c: TCtor, inp: Partial): T { + const nu = new c(inp); + if (nu instanceof Action || nu.instanceOfCheck === ClassList.action) { + this.#addAction(nu); + } else if ( + nu instanceof Section || + nu.instanceOfCheck === ClassList.section + ) { + this.#addSection(nu); + } else if ( + nu instanceof Question || + nu.instanceOfCheck === ClassList.question + ) { + this.#addQuestion(nu); + } else if (nu instanceof Page || nu.instanceOfCheck === ClassList.page) { + this.#addPage(nu); + } else if ( + nu instanceof Branch || + nu.instanceOfCheck === ClassList.branch + ) { + this.#addBranch(nu); + } else if ( + nu instanceof Result || + nu.instanceOfCheck === ClassList.result + ) { + this.#addResult(nu); + } else if (nu instanceof Config) { + this.#config = nu; + } else if ( + nu instanceof Response || + nu.instanceOfCheck === ClassList.response + ) { + this.#addResponse(nu); + } else if ( + nu instanceof Requirement || + nu.instanceOfCheck === ClassList.requirement + ) { + this.#addRequirement(nu); + } else if (nu instanceof Pages) { + this.#pages = nu; + } else if ( + nu instanceof Answer || + nu.instanceOfCheck === ClassList.answer + ) { + this.#addAnswer(nu); + } else if (nu instanceof Ref || nu.instanceOfCheck === ClassList.ref) { + this.#addRef(nu); + } + return nu; + } + + init(c: TCtor): Q { + const q = { + actions: this.#actions, + branches: this.#branches, + config: this.#config, + header: '', + pages: this.#pages, + questions: this.#questions, + results: this.#results, + sections: this.#sections, + } as Partial; + const questionnaire = new c(q); + return questionnaire; + } +} diff --git a/packages/questionable-core/src/constructable/_exports.ts b/packages/questionable-core/src/constructable/_exports.ts new file mode 100644 index 00000000..0221bc4e --- /dev/null +++ b/packages/questionable-core/src/constructable/_exports.ts @@ -0,0 +1,4 @@ +export * from './Factory'; +export * from './GateLogicCore'; +export * from './Questioner'; +export * from './SurveyBuilderCore'; diff --git a/packages/questionable-core/src/constructable/lib/TQForm.ts b/packages/questionable-core/src/constructable/lib/TQForm.ts new file mode 100644 index 00000000..c6884e86 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/TQForm.ts @@ -0,0 +1,7 @@ +import { FormCore } from '../../composable/FormCore'; +import { QuestionCore } from '../../composable/QuestionCore'; + +export type TQForm = { + form: FormCore; + question: QuestionCore; +}; diff --git a/packages/questionable-core/src/constructable/lib/TSForm.ts b/packages/questionable-core/src/constructable/lib/TSForm.ts new file mode 100644 index 00000000..947b6a93 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/TSForm.ts @@ -0,0 +1,7 @@ +import { FormCore } from '../../composable/FormCore'; +import { StepCore } from '../../composable/StepCore'; + +export type TSForm = { + form: FormCore; + step: StepCore; +}; diff --git a/packages/questionable-core/src/constructable/lib/defaultReducer.ts b/packages/questionable-core/src/constructable/lib/defaultReducer.ts new file mode 100644 index 00000000..0bc04175 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/defaultReducer.ts @@ -0,0 +1,64 @@ +import { merge } from 'lodash'; +import { eventedCore } from '../../state/pubsub'; +import { OP_TYPE, TOpType } from '../../metadata/types/TOpType'; + +export type TStepReducerAction = { + type: TOpType; + value: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +type TSgReducerFn = { + action: TStepReducerAction; + newState?: Partial; + oldState: Partial; +} +export type TReducerFn = ({ action, newState, oldState }: TSgReducerFn) => void; + +type TSgDefaultReducer = TSgReducerFn & { + Form: new (data?: Partial) => T; + callback?: TReducerFn; +} + +/** + * Merges the form's answer state as the user progresses through the survey + * @param previousState + * @param action + * @returns + */ +export function defaultReducer({ + Form, oldState, action, callback, +}: TSgDefaultReducer): T { + // Action should never be null, + // __EXCEPT__ when we attempt to storybook/test individual components in isolation + let newState: T; + switch (action?.type) { + case OP_TYPE.RESET: + newState = new Form(); + break; + + case OP_TYPE.UPDATE: + newState = new Form(merge( + { + ...oldState, + }, + { + ...action.value, + }, + )); + break; + + // Effective a noop that triggers a re-render of the page + case OP_TYPE.RERENDER: + newState = new Form(oldState); + break; + + default: + newState = new Form(oldState); + break; + } + if (callback) { + callback({ action, newState, oldState }); + } + eventedCore.publish({ event: newState, type: 'reduce' }); + return newState; +} diff --git a/packages/questionable-core/src/constructable/lib/getBirthdate.ts b/packages/questionable-core/src/constructable/lib/getBirthdate.ts new file mode 100644 index 00000000..0609e8e5 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/getBirthdate.ts @@ -0,0 +1,23 @@ +import { DateTime } from 'luxon'; +import { getDateTime } from '../../lib/date'; +import { QuestionCore } from '../../composable/QuestionCore'; +import { TQForm } from './TQForm'; + +/** + * Gets a birthdate's DateTime from a form + * @param props + * @returns + */ +export function getBirthdate({ question, form }: TQForm): DateTime | undefined { + if (!(question instanceof QuestionCore)) { + return undefined; + } + + if (question.answer) { + return getDateTime({ dt: question.answer }); + } + if (form?.birthdate) { + return getDateTime({ dt: form.birthdate }); + } + return undefined; +} diff --git a/packages/questionable-core/src/constructable/lib/isSelected.ts b/packages/questionable-core/src/constructable/lib/isSelected.ts new file mode 100644 index 00000000..e5696751 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/isSelected.ts @@ -0,0 +1,20 @@ +import { IQuestionCore } from '../../metadata/IQuestionCore'; +import { TQForm } from './TQForm'; +import { isValid } from './isValid'; + +/** + * Determines if the answer is valid and selected + * @param answer + * @param props + * @returns + */ +export function isSelected({ answer, question, form }: TQForm & { answer: string; }): boolean | undefined { + if (!form) { + return undefined; + } + const q = form.responses.find((a: IQuestionCore) => a.id === question.id); + if (!q) { + return undefined; + } + return isValid({ form, step: question }) && q.answer === answer; +} diff --git a/packages/questionable-core/src/constructable/lib/isValid.ts b/packages/questionable-core/src/constructable/lib/isValid.ts new file mode 100644 index 00000000..3f7dd2fc --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/isValid.ts @@ -0,0 +1,38 @@ +import { values } from 'lodash'; +import { TSForm } from './TSForm'; +import { STEP_TYPE } from '../../metadata/properties/type/TStepType'; + +export function isValid({ step, form }: TSForm): boolean { + const q = form.responses.find((a) => a?.id === step.id); + let ret = true; + if (!q) { + ret = false; + } + const answers = values(q?.answers); + let years = 0; + switch (q?.type) { + case STEP_TYPE.DOB: + years = form?.age?.years || 0; + if (years <= 0) { + ret = false; + } + if (!q?.exitRequirements || q.exitRequirements.length === 0) { + // ret === true + } + ret = ret + && (q.exitRequirements?.every( + (r) => r.minAge && years >= r.minAge.years, + ) + || true); + break; + case STEP_TYPE.MULTIPLE_CHOICE: + ret = ret + && q.answer !== undefined + && answers?.find((x) => x.title === q.answer) !== undefined; + break; + default: + // ret === true + break; + } + return ret; +} diff --git a/packages/questionable-core/src/constructable/lib/pools.ts b/packages/questionable-core/src/constructable/lib/pools.ts new file mode 100644 index 00000000..999e0c30 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/pools.ts @@ -0,0 +1,65 @@ +/* eslint-disable import/no-cycle */ +import { matches } from '../../lib/helpers'; +import { + isAction, + isBranch, + isQuestion, + isResult, + isSection, +} from './validators'; +import { TCollectable } from '../../metadata/types/TCollectable'; +import { TReferentialble } from '../../metadata/types/TReferentialble'; + +type TPoolName = 'questions' | 'branches' | 'sections' | 'results' | 'answers' | 'requirements' | 'actions'; +function getNameOfPool(item: T): TPoolName { + if (isQuestion(item)) { + return 'questions'; + } + if (isBranch(item)) { + return 'branches'; + } + if (isSection(item)) { + return 'sections'; + } + if (isResult(item)) { + return 'results'; + } + if (isAction(item)) { + return 'actions'; + } + throw new Error(`Collection for ${item} has not been implemented`); +} + +export function existsIn(item: T, pool: TCollectable[]): boolean { + if (Array.isArray(pool) && pool.length > 0) { + return pool.some((q) => q === item || matches(q.title, item.title)); + } + return false; +} + +export function getPool(item: T, obj: TReferentialble): TCollectable[] { + const poolName = getNameOfPool(item); + if (!obj[poolName] || !Array.isArray(obj[poolName])) { + throw new Error(`No pool has been implemented for ${poolName}`); + } + const pool = obj[poolName]; + if (!pool) { + throw new Error(`No pool has been implemented for ${poolName}`); + } + return pool; +} + +export function existsInPool(item: T, obj: TCollectable) { + const pool = getPool(item, obj); + return existsIn(item, pool); +} + +export function addToPool(item: T, obj: TCollectable) { + const pool = getPool(item, obj); + const exists = existsIn(item, pool); + if (exists) { + return obj; + } + pool.push(item); + return obj; +} diff --git a/packages/questionable-core/src/constructable/lib/toBirthdate.ts b/packages/questionable-core/src/constructable/lib/toBirthdate.ts new file mode 100644 index 00000000..0470f12e --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/toBirthdate.ts @@ -0,0 +1,30 @@ +import { QuestionCore } from '../../composable/QuestionCore'; +import { QUESTION_TYPE } from '../../metadata/properties/type/TQuestionType'; +import { TDateOfBirthCore } from '../../metadata/types/TAgeCore'; + +/** + * Converts a Date of Birth type into a string + * @param dob + * @returns + */ +export function toBirthdate({ question, dob }: { + dob: TDateOfBirthCore; + question: QuestionCore; +}): string | undefined { + if (question.type !== QUESTION_TYPE.DOB) { + return undefined; + } + if (dob.month && dob.day && dob.year) { + if (+dob.month < 1 || +dob.month > 12) { + return undefined; + } + if (+dob.day < 1 || +dob.day > 31) { + return undefined; + } + if (+dob.year < 1900 || +dob.year > new Date().getFullYear()) { + return undefined; + } + return `${dob.month.padStart(2, '0')}/${dob.day.padStart(2, '0')}/${dob.year}`; + } + return undefined; +} diff --git a/packages/questionable-core/src/constructable/lib/toString.ts b/packages/questionable-core/src/constructable/lib/toString.ts new file mode 100644 index 00000000..107e60f4 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/toString.ts @@ -0,0 +1,8 @@ +import { QuestionCore } from '../../composable/QuestionCore'; + +export function toString({ question }: { question: QuestionCore; }): string { + if (!question.title || question.title === undefined || question.title?.length <= 0) { + throw new Error(`Value is required; ${question.id} does not have a title`); + } + return question.title; +} diff --git a/packages/questionable-core/src/constructable/lib/updateForm.ts b/packages/questionable-core/src/constructable/lib/updateForm.ts new file mode 100644 index 00000000..3ea6912f --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/updateForm.ts @@ -0,0 +1,37 @@ +import { merge } from 'lodash'; +import { eventedCore } from '../../state/pubsub'; +import { TQForm } from './TQForm'; +import { OP_TYPE } from '../../metadata/types/TOpType'; + +/** + * Updates the form with the current selected answer(s) + * @param answer + * @param props + * @returns + */ +export function updateForm({ answer, question, form }: TQForm & { answer: string; }): void { + if (answer?.length > 0) { + merge(question, { answer }); + } + // TODO: circle back and fix this logic. The problem is that our reducer is merging by KEY, + // which in the case of arrays is the index, and the index will always be 0 if we're passing in new arrays + // There are cleaner ways to do this. + // eslint-disable-next-line no-param-reassign + form.responses = form.responses || []; + const value = form.responses.find((r) => r.id === question.id); + if (!value) { + form.responses.push(question); + } else { + merge(value, question); + } + eventedCore.publish({ + event: { answer, props: question, question: question.id }, + type: 'answer', + }); + form.reduce({ + action: { + type: OP_TYPE.UPDATE, + value: form, + }, + }); +} diff --git a/packages/questionable-core/src/constructable/lib/validators.ts b/packages/questionable-core/src/constructable/lib/validators.ts new file mode 100644 index 00000000..82c38eb5 --- /dev/null +++ b/packages/questionable-core/src/constructable/lib/validators.ts @@ -0,0 +1,39 @@ +import { isEnum } from '../../lib/enums'; +import { TTypeable } from '../../metadata/types/TTypeable'; +import { ClassList } from '../../lib/instanceOf'; +import { BRANCH_TYPE } from '../../metadata/properties/type/TBranchType'; +import { ACTION_TYPE } from '../../metadata/properties/type/TActionType'; +import { PAGE_TYPE } from '../../metadata/properties/type/TPageType'; +import { QUESTION_TYPE } from '../../metadata/properties/type/TQuestionType'; +import { RESULT_TYPE } from '../../metadata/properties/type/TResultType'; +import { SECTION_TYPE } from '../../metadata/properties/type/TSectionType'; +import { STEP_TYPE } from '../../metadata/properties/type/TStepType'; + +export function isAction(value: T): value is T { + return (value.instanceOfCheck === ClassList.action || isEnum({ enm: ACTION_TYPE, value: value.type }) === true); +} + +export function isBranch(value: T): value is T { + return (value.instanceOfCheck === ClassList.branch || isEnum({ enm: BRANCH_TYPE, value: value.type }) === true); +} + +export function isPage(value: T): value is T { + return (value.instanceOfCheck === ClassList.page || isEnum({ enm: PAGE_TYPE, value: value.type }) === true); +} + +export function isQuestion(value: T): value is T { + return (value.instanceOfCheck === ClassList.question || isEnum({ enm: QUESTION_TYPE, value: value.type }) === true); +} + +export function isResult(value: T): value is T { + return (value.instanceOfCheck === ClassList.result || isEnum({ enm: RESULT_TYPE, value: value.type }) === true); +} + +export function isSection(value: T): value is T { + return (value.instanceOfCheck === ClassList.section || isEnum({ enm: SECTION_TYPE, value: value.type }) === true); +} + +export function isStep(value: T): value is T { + return (value.instanceOfCheck === ClassList.step + || isEnum({ enm: STEP_TYPE, value: value.type }) || isPage(value) || isQuestion(value)); +} diff --git a/packages/questionable-core/src/index.ts b/packages/questionable-core/src/index.ts index 29539d85..9a8cdd24 100644 --- a/packages/questionable-core/src/index.ts +++ b/packages/questionable-core/src/index.ts @@ -1,6 +1,6 @@ -export * from './composable'; -export * from './lib'; -export * from './schema'; -export * from './state'; -export * from './survey'; -export * from './util'; +export * from './composable/_exports'; +export * from './constructable/_exports'; +export * from './schema/_exports'; +export * from './state/_exports'; +export * from './metadata/_exports'; +export * from './lib/_exports'; diff --git a/packages/questionable-core/src/lib/QuestionsCore.ts b/packages/questionable-core/src/lib/QuestionsCore.ts deleted file mode 100644 index 01b3348e..00000000 --- a/packages/questionable-core/src/lib/QuestionsCore.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { merge } from 'lodash'; -import { DateTime } from 'luxon'; -import { QuestionableConfigCore } from '../composable/QuestionableConfigCore'; -import { IQuestionCore } from '../survey'; -import { IQuestionDataCore } from '../survey/IQuestionDataCore'; -import { IRefCore } from '../survey/IRefCore'; -import { getDateTime } from '../util/date'; -import { ACTION_TYPE } from '../util/enums'; -import { TDateOfBirthCore } from '../util/types'; -import { StepsCore } from './StepsCore'; - -/** - * Static utility methods for question components - */ -export abstract class QuestionsCore { - /** - * Updates the form with the current selected answer(s) - * @param answer - * @param props - * @returns - */ - public static updateForm( - answer: string, - props: IQuestionDataCore, - config: QuestionableConfigCore = new QuestionableConfigCore(), - ): void { - if (answer.length > 0) { - Object.assign(props.step, { answer }); - } - // TODO: circle back and fix this logic. The problem is that our reducer is merging by KEY, - // which in the case of arrays is the index, and the index will always be 0 if we're passing in new arrays - // There are cleaner ways to do this. - // eslint-disable-next-line no-param-reassign - props.form.responses = props.form.responses || []; - const value = props.form.responses.find((r) => r.id === props.step.id); - if (!value) { - props.form.responses.push(props.step); - } else { - merge(value, props.step); - } - if (config.events?.answer) { - config.events.answer({ answer, props, step: props.step.id }); - } - return props.dispatchForm({ - type: ACTION_TYPE.UPDATE, - value: { ...props.form }, - }); - } - - /** - * Determines if the answer is valid and selected - * @param answer - * @param props - * @returns - */ - protected static isSelected( - answer: string, - props: IQuestionDataCore, - ): boolean | undefined { - if (!props?.form) { - return undefined; - } - const q: IQuestionCore | undefined = props.form.responses.find( - (a: IQuestionCore) => a.id === props.step.id, - ); - if (!q) { - return undefined; - } - return StepsCore.isValid(props.form, props.step.id) && q.answer === answer; - } - - protected static getString(ref: Partial = {}): string { - if (!ref.title || ref.title === undefined || ref.title?.length <= 0) { - throw new Error(`Value is required; ${ref.id} does not have a title`); - } - return ref.title; - } - - /** - * Gets a birthdate's DateTime from a form - * @param props - * @returns - */ - public static getBirthdate(props: IQuestionDataCore): DateTime | undefined { - if (props.step?.answer) { - return getDateTime(props.step.answer); - } - if (props.form?.birthdate) { - return getDateTime(props.form.birthdate); - } - return undefined; - } - - /** - * Converts a Date of Birth type into a string - * @param dob - * @returns - */ - public static toBirthdate(dob: TDateOfBirthCore): string | undefined { - if (dob.month && dob.day && dob.year) { - if (+dob.month < 1 || +dob.month > 12) { - return undefined; - } - if (+dob.day < 1 || +dob.day > 31) { - return undefined; - } - if (+dob.year < 1900 || +dob.year > new Date().getFullYear()) { - return undefined; - } - return `${dob.month.padStart(2, '0')}/${dob.day.padStart(2, '0')}/${dob.year}`; - } - return undefined; - } -} diff --git a/packages/questionable-core/src/util/README.md b/packages/questionable-core/src/lib/README.md similarity index 100% rename from packages/questionable-core/src/util/README.md rename to packages/questionable-core/src/lib/README.md diff --git a/packages/questionable-core/src/lib/StepsCore.ts b/packages/questionable-core/src/lib/StepsCore.ts deleted file mode 100644 index d071a685..00000000 --- a/packages/questionable-core/src/lib/StepsCore.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { kebabCase, values } from 'lodash'; -import { QuestionnaireCore } from '../composable'; -import { noop } from '../util/noop'; -import { - DIRECTION, isEnum, PAGE_TYPE, QUESTION_TYPE, STEP_TYPE, -} from '../util/enums'; -import { IFormCore } from '../survey/IFormCore'; -import { IStepDataCore } from '../survey/IStepDataCore'; -import { IQuestionDataCore } from '../survey/IQuestionDataCore'; -import { IStepCore } from '../survey'; - -export abstract class StepsCore { - public static goToStep(step: string, props: IStepDataCore, cb = noop): void { - if (cb) { - cb(step, props); - } - } - - public static goToNextStep( - props: IStepDataCore, - questionnaire: QuestionnaireCore, - ): void { - const step = questionnaire.getNextStep(props); - const dir = DIRECTION.FORWARD; - questionnaire.config.events.page({ dir, props, step }); - StepsCore.goToStep(step, props); - } - - public static goToPrevStep( - props: IStepDataCore, - questionnaire: QuestionnaireCore, - ): void { - const step = questionnaire.getPreviousStep(props); - const dir = DIRECTION.BACKWARD; - questionnaire.config.events.page({ dir, props, step }); - StepsCore.goToStep(step, props); - } - - /** - * Determines whether the user should be allowed to continue - * @param props - * @returns - */ - public static isNextEnabled(props: IStepDataCore): boolean { - if (!props?.step) { - throw new Error('This survery is not defined'); - } - if (props.stepId === STEP_TYPE.LANDING) { - return true; - } - if (props.stepId === STEP_TYPE.SUMMARY) { - return true; - } - // KLUDGE Alert: this is not an elegant way to solve this - if (props.step?.type === QUESTION_TYPE.DOB) { - const yearsOld = props.form.age?.years || 0; - return yearsOld > 0; - } - if (!props.form) { - return false; - } - return StepsCore.isValid(props.form, props.step?.id); - } - - public static isValid(form: IFormCore, questionId: string): boolean { - const q = form.responses.find((a) => a?.id === questionId); - let ret = true; - if (!q) { - ret = false; - } - const answers = values(q?.answers); - let years = 0; - switch (q?.type) { - case STEP_TYPE.DOB: - years = form?.age?.years || 0; - if (years <= 0) { - ret = false; - } - if (!q?.exitRequirements || q.exitRequirements.length === 0) { - // ret === true - } - ret = ret - && (q.exitRequirements?.every((r) => r.minAge && years >= r.minAge.years) || true); - break; - case STEP_TYPE.MULTIPLE_CHOICE: - ret = ret && ( - q.answer !== undefined - && answers?.find((x) => x.title === q.answer) !== undefined - ); - break; - default: - // ret === true - break; - } - return ret; - } - - public static getFieldSetName(props: IQuestionDataCore): string { - return kebabCase(props.step.title); - } - - public static getDomId(answer: string, props: IQuestionDataCore): string { - const name = StepsCore.getFieldSetName(props); - return `${name}-${kebabCase(answer)}`; - } - - public static getStepType(step: IStepCore) { - if (isEnum(QUESTION_TYPE, step.type)) { - return 'question'; - } - if (isEnum(PAGE_TYPE, step.type)) { - return 'page'; - } - return 'unknown'; - } -} diff --git a/packages/questionable-core/src/util/index.ts b/packages/questionable-core/src/lib/_exports.ts similarity index 53% rename from packages/questionable-core/src/util/index.ts rename to packages/questionable-core/src/lib/_exports.ts index 00ead7a5..dc205bde 100644 --- a/packages/questionable-core/src/util/index.ts +++ b/packages/questionable-core/src/lib/_exports.ts @@ -3,6 +3,11 @@ export * from './date'; export * from './enums'; export * from './error'; export * from './helpers'; -export * from './log'; +export * from './instanceOf'; +export * from './labels'; +export * from './logger'; +export * from './merge'; export * from './noop'; +export * from './set'; export * from './types'; +export * from './uuid'; diff --git a/packages/questionable-core/src/util/array.ts b/packages/questionable-core/src/lib/array.ts similarity index 77% rename from packages/questionable-core/src/util/array.ts rename to packages/questionable-core/src/lib/array.ts index 1ddfc801..19ca6694 100644 --- a/packages/questionable-core/src/util/array.ts +++ b/packages/questionable-core/src/lib/array.ts @@ -10,10 +10,11 @@ interface IGrouped { * @param prop A property name to group by * @returns IGrouped */ -export const groupBy = (array: T[], prop: string): IGrouped => - array.reduce((groups: IGrouped, item: any) => { +export function groupBy({ array, prop }: { array: T[]; prop: string; }): IGrouped { + return array.reduce((groups: IGrouped, item: any) => { const val = item[prop]; groups[val] = groups[val] || []; groups[val].push(item); return groups; }, {}); +} diff --git a/packages/questionable-core/src/util/date.ts b/packages/questionable-core/src/lib/date.ts similarity index 68% rename from packages/questionable-core/src/util/date.ts rename to packages/questionable-core/src/lib/date.ts index 04986fb8..e147ae71 100644 --- a/packages/questionable-core/src/util/date.ts +++ b/packages/questionable-core/src/lib/date.ts @@ -1,34 +1,36 @@ import { DateTime } from 'luxon'; -import { TAgeCore } from './types'; +import { TAgeCore } from '../metadata/types/TAgeCore'; /** * Determines if a string can be parsed into a valid Date * @param dt * @returns */ -export const isValidDate = (dt: string | undefined): boolean => !(!dt || dt.length < 8); +export function isValidDate({ dt }: { dt: string | undefined; }): boolean { + return !(!dt || dt.length < 8); +} /** * Gets a luxon DateTime object from a date string * @param dt DateTime as string- should always be in the format `MM/DD/YYYY` * @returns DateTime or undefined */ -export const getDateTime = (dt: string): DateTime | undefined => { - if (!isValidDate(dt)) return undefined; +export function getDateTime({ dt }: { dt: string; }): DateTime | undefined { + if (!isValidDate({ dt })) return undefined; const date = new Date( +dt.substring(6, 10), +dt.substring(0, 2) - 1, +dt.substring(3, 5), ); return DateTime.fromJSDate(date); -}; +} /** * Gets an age from a DateTime object * @param dob - luxon DateTime * @returns an age with years, months, days */ -export const getDateTimeAge = (dob: DateTime): TAgeCore => { +export function getDateTimeAge({ dob }: { dob: DateTime; }): TAgeCore { const now = DateTime.now(); const yearNow = now.year; @@ -67,18 +69,22 @@ export const getDateTimeAge = (dob: DateTime): TAgeCore => { months, years, }; -}; +} /** * Parses a date/time string and returns an Age object * @param dateOfBirth - should always be in the format `MM/DD/YYYY` * @returns an age, if the date is valid */ -export const getAge = (dateOfBirth: string | undefined): TAgeCore | undefined => { - if (!dateOfBirth || !isValidDate(dateOfBirth)) return undefined; +export function getAge({ dateOfBirth }: { dateOfBirth: string | undefined; }): TAgeCore | undefined { + if (!dateOfBirth || !isValidDate({ dt: dateOfBirth })) { + return undefined; + } - const dob = getDateTime(dateOfBirth); - if (!dob) return undefined; + const dob = getDateTime({ dt: dateOfBirth }); + if (!dob) { + return undefined; + } - return getDateTimeAge(dob); -}; + return getDateTimeAge({ dob }); +} diff --git a/packages/questionable-core/src/lib/enums.ts b/packages/questionable-core/src/lib/enums.ts new file mode 100644 index 00000000..92529fbf --- /dev/null +++ b/packages/questionable-core/src/lib/enums.ts @@ -0,0 +1,26 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export function isEnum({ enm, value }: { enm: object; value: string; }): boolean { + return Object.values(enm).includes(value); +} + +// Traditional enums + +export enum DATE_UNIT { + DAY = 'day', + MONTH = 'month', + YEAR = 'year', +} + +export enum MODE { + DEV = 'dev', + EDIT = 'edit', + VIEW = 'view', +} + +/** + * Navigation direction for steps by array index (+1 or -1) + */ +export enum DIRECTION { + FORWARD = 1, + BACKWARD = -1, +} diff --git a/packages/questionable-core/src/util/error.ts b/packages/questionable-core/src/lib/error.ts similarity index 88% rename from packages/questionable-core/src/util/error.ts rename to packages/questionable-core/src/lib/error.ts index 7e64dab1..c8b807f2 100644 --- a/packages/questionable-core/src/util/error.ts +++ b/packages/questionable-core/src/lib/error.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -export const catchError = (e: any): Error => { +export function catchError({ e }: { e: any; }): Error { if (e instanceof Error) { return e; } @@ -14,4 +14,4 @@ export const catchError = (e: any): Error => { return new Error(`${e}`); } throw new Error(`${e}`); -}; +} diff --git a/packages/questionable-core/src/lib/factories.ts b/packages/questionable-core/src/lib/factories.ts new file mode 100644 index 00000000..064f8505 --- /dev/null +++ b/packages/questionable-core/src/lib/factories.ts @@ -0,0 +1,88 @@ +import { noop } from 'lodash'; +import { + RequirementCore, + AnswerCore, + BranchCore, + PageCore, + SectionCore, + ActionCore, + ResponseCore, + QuestionCore, + RefCore, +} from '../composable/_exports'; +import { ResultCore } from '../composable/ResultCore'; +import { TInstanceOf } from './instanceOf'; + +export class Factories { + gen( + cls: TInstanceOf, + data: T[], + ): RequirementCore[]; + + gen(cls: TInstanceOf, data: T[]): ResultCore[]; + + gen(cls: TInstanceOf, data: T[]): AnswerCore[]; + + gen(cls: TInstanceOf, data: T[]): BranchCore[]; + + gen(cls: TInstanceOf, data: T[]): PageCore[]; + + gen(cls: TInstanceOf, data: T[]): SectionCore[]; + + gen(cls: TInstanceOf, data: T[]): ActionCore[]; + + gen(cls: TInstanceOf, data: T[]): ResponseCore[]; + + gen(cls: TInstanceOf, data: T[]): QuestionCore[]; + + gen(cls: TInstanceOf, inp: T[]): RefCore[] { + noop(cls, inp); + throw new Error('Not implemented'); + } + + genOne(cls: TInstanceOf, data: T): RequirementCore; + + genOne(cls: TInstanceOf, data: T): ResultCore; + + genOne(cls: TInstanceOf, data: T): AnswerCore; + + genOne(cls: TInstanceOf, data: T): BranchCore; + + genOne(cls: TInstanceOf, data: T): PageCore; + + genOne(cls: TInstanceOf, data: T): SectionCore; + + genOne(cls: TInstanceOf, data: T): ActionCore; + + genOne(cls: TInstanceOf, data: T): ResponseCore; + + genOne(cls: TInstanceOf, data: T): QuestionCore; + + genOne(cls: TInstanceOf, inp: T): RefCore { + noop(cls, inp); + return inp; + // const data = merge(this.#defaults, inp); + // switch (cls) { + // case ClassList.action: + // return this.addAction(data as ActionCore); + // case ClassList.answer: + // return this.addAnswer(data as AnswerCore); + // case ClassList.branch: + // return this.addBranch(data as BranchCore); + // case ClassList.page: + // return this.addPage(data as PageCore); + // case ClassList.question: + // return this.addQuestion(data as QuestionCore); + // case ClassList.requirement: + // return this.addRequirement(data as RequirementCore); + // case ClassList.response: + // return this.addResponse(data as ResponseCore); + // case ClassList.result: + // return this.addResult(data as ResultCore); + // case ClassList.section: + // return this.addSection(data as SectionCore); + // default: + // return this.addRef(inp); + // } + } +} diff --git a/packages/questionable-core/src/util/helpers.ts b/packages/questionable-core/src/lib/helpers.ts similarity index 100% rename from packages/questionable-core/src/util/helpers.ts rename to packages/questionable-core/src/lib/helpers.ts diff --git a/packages/questionable-core/src/lib/index.ts b/packages/questionable-core/src/lib/index.ts deleted file mode 100644 index 40478d84..00000000 --- a/packages/questionable-core/src/lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './StepsCore'; -export * from './QuestionsCore'; diff --git a/packages/questionable-core/src/lib/instanceOf.ts b/packages/questionable-core/src/lib/instanceOf.ts new file mode 100644 index 00000000..7c25879c --- /dev/null +++ b/packages/questionable-core/src/lib/instanceOf.ts @@ -0,0 +1,267 @@ +import { isEmpty } from 'lodash'; +import { BaseCore } from '../composable/BaseCore'; +import { ClassProperties } from '../metadata/types/ClassProperties'; + +enum EClassList { + ACTION = 'action', + ANSWER = 'answer', + BASE = 'base', + BRANCH = 'branch', + BUTTON = 'button', + BUTTONS = 'buttons', + CONFIG = 'config', + DESIGN = 'design', + DESIGN_DATA = 'design-data', + EVENT = 'event', + EVENT_EMITTER = 'event-emitter', + FORM = 'form', + PAGE = 'page', + PAGES = 'pages', + PAGE_DATA = 'page-data', + QUESTION = 'question', + QUESTIONNAIRE = 'questionnaire', + QUESTIONS = 'questions', + QUESTION_DATA = 'question-data', + REF = 'ref', + REQUIREMENT = 'requirement', + RESPONSE = 'response', + RESULT = 'result', + RESULTS = 'results', + SECTION = 'section', + STEP = 'step', + STEPS = 'steps', + STEP_DATA = 'step-data', + SURVEY = 'survey', + SURVEY_BUILDER = 'survey-builder' +} + +type TPrefix = { + ACTION: EClassList.ACTION, + ANSWER: EClassList.ANSWER, + BASE: EClassList.BASE, + BRANCH: EClassList.BRANCH, + BUTTON: EClassList.BUTTON, + BUTTONS: EClassList.BUTTONS, + CONFIG: EClassList.CONFIG, + DESIGN: EClassList.DESIGN, + DESIGN_DATA: EClassList.DESIGN_DATA, + EVENT: EClassList.EVENT, + EVENT_EMITTER: EClassList.EVENT_EMITTER, + FORM: EClassList.FORM, + PAGE: EClassList.PAGE, + PAGES: EClassList.PAGES, + PAGE_DATA: EClassList.PAGE_DATA, + QUESTION: EClassList.QUESTION, + QUESTIONNAIRE: EClassList.QUESTIONNAIRE, + QUESTIONS: EClassList.QUESTIONS, + QUESTION_DATA: EClassList.QUESTION_DATA, + REF: EClassList.REF, + REQUIREMENT: EClassList.REQUIREMENT, + RESPONSE: EClassList.RESPONSE, + RESULT: EClassList.RESULT, + RESULTS: EClassList.RESULTS, + SECTION: EClassList.SECTION, + STEP: EClassList.STEP, + STEPS: EClassList.STEPS, + STEP_DATA: EClassList.STEP_DATA, + SURVEY: EClassList.SURVEY, + SURVEY_BUILDER: EClassList.SURVEY_BUILDER, +}; + +const CLASS_NAME: TPrefix = { + ACTION: EClassList.ACTION, + ANSWER: EClassList.ANSWER, + BASE: EClassList.BASE, + BRANCH: EClassList.BRANCH, + BUTTON: EClassList.BUTTON, + BUTTONS: EClassList.BUTTONS, + CONFIG: EClassList.CONFIG, + DESIGN: EClassList.DESIGN, + DESIGN_DATA: EClassList.DESIGN_DATA, + EVENT: EClassList.EVENT, + EVENT_EMITTER: EClassList.EVENT_EMITTER, + FORM: EClassList.FORM, + PAGE: EClassList.PAGE, + PAGES: EClassList.PAGES, + PAGE_DATA: EClassList.PAGE_DATA, + QUESTION: EClassList.QUESTION, + QUESTIONNAIRE: EClassList.QUESTIONNAIRE, + QUESTIONS: EClassList.QUESTIONS, + QUESTION_DATA: EClassList.QUESTION_DATA, + REF: EClassList.REF, + REQUIREMENT: EClassList.REQUIREMENT, + RESPONSE: EClassList.RESPONSE, + RESULT: EClassList.RESULT, + RESULTS: EClassList.RESULTS, + SECTION: EClassList.SECTION, + STEP: EClassList.STEP, + STEPS: EClassList.STEPS, + STEP_DATA: EClassList.STEP_DATA, + SURVEY: EClassList.SURVEY, + SURVEY_BUILDER: EClassList.SURVEY_BUILDER, +}; + +interface IClassMap { + [CLASS_NAME.ACTION]: TInstanceOf; + [CLASS_NAME.ANSWER]: TInstanceOf; + [CLASS_NAME.BASE]: TInstanceOf; + [CLASS_NAME.BRANCH]: TInstanceOf; + [CLASS_NAME.BUTTON]: TInstanceOf; + [CLASS_NAME.BUTTONS]: TInstanceOf; + [CLASS_NAME.CONFIG]: TInstanceOf; + [CLASS_NAME.DESIGN]: TInstanceOf; + [CLASS_NAME.DESIGN_DATA]: TInstanceOf; + [CLASS_NAME.EVENT]: TInstanceOf; + [CLASS_NAME.EVENT_EMITTER]: TInstanceOf; + [CLASS_NAME.FORM]: TInstanceOf; + [CLASS_NAME.PAGE]: TInstanceOf; + [CLASS_NAME.PAGES]: TInstanceOf; + [CLASS_NAME.PAGE_DATA]: TInstanceOf; + [CLASS_NAME.QUESTION]: TInstanceOf; + [CLASS_NAME.QUESTIONNAIRE]: TInstanceOf; + [CLASS_NAME.QUESTIONS]: TInstanceOf; + [CLASS_NAME.QUESTION_DATA]: TInstanceOf; + [CLASS_NAME.REF]: TInstanceOf; + [CLASS_NAME.REQUIREMENT]: TInstanceOf; + [CLASS_NAME.RESPONSE]: TInstanceOf; + [CLASS_NAME.RESULT]: TInstanceOf; + [CLASS_NAME.RESULTS]: TInstanceOf; + [CLASS_NAME.SECTION]: TInstanceOf; + [CLASS_NAME.STEP]: TInstanceOf; + [CLASS_NAME.STEPS]: TInstanceOf; + [CLASS_NAME.STEP_DATA]: TInstanceOf; + [CLASS_NAME.SURVEY]: TInstanceOf; + [CLASS_NAME.SURVEY_BUILDER]: TInstanceOf; +} + +type TEPrefix = ClassProperties; + +const SUFFIX: { + CORE: 'core', + DEFAULT: 'default', +} = { + CORE: 'core' as const, + DEFAULT: 'default' as const, +}; + +type TESuffix = ClassProperties; + +const COI: { + CLASS: '', + INTERFACE: 'I', +} = { + CLASS: '' as const, + INTERFACE: 'I' as const, +}; + +type TECoi = ClassProperties; + + type TInstanceOf = + `${TECoi}${TEPrefix}-${TESuffix}` + | `${TECoi}${TEPrefix}s-${TESuffix}` + | `${TECoi}${TEPrefix}s` + | `${TECoi}${TEPrefix}`; + +function makeName({ name, suffix, coi }: + { coi: TECoi; name: TEPrefix; suffix?: TESuffix | ''; }): TInstanceOf; +function makeName( + { name, suffix = SUFFIX.CORE, coi = false }: { coi?: boolean | TECoi; name: TEPrefix; suffix?: TESuffix | ''; }, +): TInstanceOf { + const isInterface = coi === true || coi === COI.INTERFACE; + let ret: TInstanceOf = `${isInterface ? COI.INTERFACE : COI.CLASS}${name}`; + if (suffix !== '') { + ret = `${isInterface ? COI.INTERFACE : COI.CLASS}${name}-${suffix}`; + } + return ret; +} + +function getClassName({ name, suffix = SUFFIX.CORE }: { name: TEPrefix; suffix?: TESuffix; }): TInstanceOf { + return makeName({ coi: COI.CLASS, name, suffix }); +} + +function getInterfaceName({ name, suffix = SUFFIX.CORE }: { name: TEPrefix; suffix?: TESuffix; }): TInstanceOf { + return makeName({ coi: COI.INTERFACE, name, suffix }); +} + +function extractNames({ name }: { name: TInstanceOf; }): TInstanceOf[] { + const names: TInstanceOf[] = [name]; + let parts: string[]; + let coi: TECoi = COI.CLASS; + if (name.startsWith(COI.INTERFACE)) { + coi = COI.INTERFACE; + parts = name.replace('I', '').split('-'); + } else { + parts = name.split('-'); + } + const prefix: TEPrefix = parts[0] as TEPrefix; + const suffix: TESuffix | '' = (isEmpty(parts[1]) ? '' : parts[1] as TESuffix); + names.push(makeName({ coi, name: prefix, suffix: '' })); + if (suffix !== '') { + names.push(makeName({ coi, name: prefix, suffix })); + } else { + names.push(makeName({ coi, name: prefix, suffix: SUFFIX.CORE })); + } + if (names.some((n) => n.startsWith(CLASS_NAME.REF))) { + names.push('base-core'); + } + return names; +} + +function getClassesAndInterfaces({ names }: { names: TInstanceOf[]; }): TInstanceOf[] { + const ret = names.concat(names.reduce((_p, currentValue, _i, out) => + out.concat(extractNames({ name: currentValue })), names)); + return [...new Set([...ret])]; +} + +function checkInstanceOf({ names, obj }: { names: TInstanceOf[]; obj: BaseCore; }) { + const checking = getClassesAndInterfaces({ names }); + return checking.some((i) => `${i}` === `${obj?.instanceOfCheck}`); +} + +type instanceMap = Map; + +function assembleClassNames() { + const ret: instanceMap = new Map(); + + for (const name of Object.values(CLASS_NAME)) { + ret.set(name, getClassName({ name })); + } + return ret; +} + +const ClassMap = assembleClassNames(); + +function getInstanceName({ name }: { name: T; }): TInstanceOf { + return ClassMap.get(name)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion +} + +const classes = {} as IClassMap; +Object.values(CLASS_NAME).forEach((key) => { + classes[key] = getInstanceName({ name: key }); +}); + +const ClassList = { ...classes } as IClassMap; + +export { + assembleClassNames, + checkInstanceOf, + ClassMap, + ClassList, + EClassList, + COI, + extractNames, + getClassesAndInterfaces, + getClassName, + getInstanceName, + getInterfaceName, + makeName, + CLASS_NAME as PREFIX, + CLASS_NAME, + SUFFIX, + type instanceMap, + type TECoi, + type TEPrefix, + type TESuffix, + type TInstanceOf, + type TPrefix, +}; diff --git a/packages/questionable-core/src/lib/labels.ts b/packages/questionable-core/src/lib/labels.ts new file mode 100644 index 00000000..aa9bcd9c --- /dev/null +++ b/packages/questionable-core/src/lib/labels.ts @@ -0,0 +1,89 @@ +type alphabet = + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z'; +const alphaSeq: alphabet[] = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', +]; + +function makeLabelGenerator() { + const used: { [key: string]: boolean; } = {}; + let iteration = 0; + let lastIssued = 0; + + function generate(idx = lastIssued): { label: string; next: number; } { + let next = idx; + if (next >= alphaSeq.length) { + next = 0; + iteration += 1; + } + let label = alphaSeq[next]; + if (iteration > 0) { + label += `${iteration}`; + } + if (used[label] === true) { + return generate(next + 1); + } + return { + label, + next, + }; + } + + return (): string => { + const nis = generate(lastIssued); + used[nis.label] = true; + lastIssued = nis.next; + return nis.label; + }; +} + +export const getNextLabel = makeLabelGenerator(); diff --git a/packages/questionable-core/src/lib/logger.ts b/packages/questionable-core/src/lib/logger.ts new file mode 100644 index 00000000..c86658f9 --- /dev/null +++ b/packages/questionable-core/src/lib/logger.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import chalk from 'chalk'; +import _log from 'loglevel'; + +class Logger { + static toggleOut(t?: 'on' | 'off' | true | false) { + if (t === 'on' || t === true) { + _log.enableAll(true); + } else { + _log.enableAll(false); + } + } + + static log(...msg: any[]) { + return _log.log(...msg); + } + + static info(...msg: any[]) { + return _log.info(...msg); + } + + static warn(...msg: any[]) { + return _log.warn(...msg); + } + + // TODO: add callstack and error serialization + static error(...msg: any[]) { + return _log.error(...msg); + } + + static getBaseLogger() { + return _log; + } + + static white(...msg: any[]) { + return Logger.log(chalk.white(...msg)); + } + + static yellow(...msg: any[]) { + return Logger.log(chalk.yellow(...msg)); + } + + static blue(...msg: any[]) { + return Logger.log(chalk.blue(...msg)); + } + + static red(...msg: any[]) { + return Logger.log(chalk.red(...msg)); + } +} + +export const { + log, + info, + warn, + error, + getBaseLogger, + white, + yellow, + blue, + toggleOut, + red, +} = Logger; diff --git a/packages/questionable-core/src/lib/merge.ts b/packages/questionable-core/src/lib/merge.ts new file mode 100644 index 00000000..05d9ee74 --- /dev/null +++ b/packages/questionable-core/src/lib/merge.ts @@ -0,0 +1,72 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + isArray, + isEmpty, + isFunction, + isNumber, + isString, + mergeWith, + noop, +} from 'lodash'; + +// eslint-disable-next-line consistent-return +function customizer(objValue: any, srcValue: any): any | void { + if (isArray(objValue) && isArray(srcValue)) { + return objValue.concat(srcValue); + } + if (isEmpty(objValue)) { + return srcValue; + } + if (isEmpty(srcValue)) { + return objValue; + } + if (isFunction(objValue) + && (srcValue === noop + || isEmpty(srcValue) + || srcValue === null + || srcValue === undefined)) { + return objValue; + } + if (isFunction(srcValue) + && (objValue === noop + || isEmpty(objValue) + || objValue === null + || objValue === undefined)) { + return srcValue; + } + if (isString(objValue) || isString(srcValue)) { + if (isEmpty(srcValue) + || srcValue === null + || srcValue === undefined + || !srcValue) { + return objValue; + } + if (isEmpty(objValue) + || objValue === null + || objValue === undefined + || !objValue) { + return srcValue; + } + return srcValue || objValue; + } + if (isNumber(objValue) || isNumber(srcValue)) { + if (isEmpty(srcValue) + || srcValue === null + || srcValue === undefined + || srcValue + ''.length === 0) { + return objValue; + } + if (isEmpty(objValue) + || objValue === null + || objValue === undefined + || objValue + ''.length === 0) { + return srcValue; + } + return srcValue || objValue; + } +} + +export function merge({ params = [] }: { params?: any[]; } = {}) { + return mergeWith([...params], customizer); +} diff --git a/packages/questionable-core/src/util/noop.ts b/packages/questionable-core/src/lib/noop.ts similarity index 51% rename from packages/questionable-core/src/util/noop.ts rename to packages/questionable-core/src/lib/noop.ts index 5368bd18..2cfc52cb 100644 --- a/packages/questionable-core/src/util/noop.ts +++ b/packages/questionable-core/src/lib/noop.ts @@ -1,7 +1,12 @@ +import { noop } from 'lodash'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-empty-function */ /** * Generic no-operation */ -export const noop = (..._params: unknown[]): void => { }; +export { noop }; + +export async function noopAsync(..._params: unknown[]) { + return noop(..._params); +} diff --git a/packages/questionable-core/src/lib/set.ts b/packages/questionable-core/src/lib/set.ts new file mode 100644 index 00000000..4cd4377a --- /dev/null +++ b/packages/questionable-core/src/lib/set.ts @@ -0,0 +1,31 @@ +import { + isEmpty, isString, uniq, uniqBy, +} from 'lodash'; + +type IRefCore = { + id: string; + title: string; + type: string; +} + +export function toSet( + { data, join = new Set() }: { data: T[]; join?: Set; }, +): Set { + if (isEmpty(data) && isEmpty(join)) { + return new Set(); + } + const existing = Array.from(join); + const union = data.concat(existing); + let unique: T[] = []; + + if (isString(data[0]) || isString(existing[0])) { + unique = uniq(union); + } else { + unique = uniqBy(union, 'id'); + } + return new Set(unique); +} + +export function fromSet({ data }: { data: Set; }) { + return Array.from(data); +} diff --git a/packages/questionable-core/src/lib/types.ts b/packages/questionable-core/src/lib/types.ts new file mode 100644 index 00000000..03f4213f --- /dev/null +++ b/packages/questionable-core/src/lib/types.ts @@ -0,0 +1,11 @@ +/** + * Lambda that can be called to compute an age requirement + * @hidden Functions must be hidden from schema + */ +export type TAgeCalcCore = (birthdate: string) => boolean; +/** + * @hidden Functions must be hidden from schema + */ +export type TReducerCore = (...params: unknown[]) => void; + +export type TPointerDirection = 'in' | 'out'; diff --git a/packages/questionable-core/src/lib/uuid.ts b/packages/questionable-core/src/lib/uuid.ts new file mode 100644 index 00000000..2bcdf95f --- /dev/null +++ b/packages/questionable-core/src/lib/uuid.ts @@ -0,0 +1,11 @@ +import { v4 } from 'uuid'; +import ShortUniqueId from 'short-unique-id'; + +const shortUuid = new ShortUniqueId({ length: 6 }); + +export function getGUID({ short = true }: { short?: boolean; } = {}) { + if (short) { + return shortUuid(); + } + return v4(); +} diff --git a/packages/questionable-core/src/survey/IActionCore.ts b/packages/questionable-core/src/metadata/IActionCore.ts similarity index 77% rename from packages/questionable-core/src/survey/IActionCore.ts rename to packages/questionable-core/src/metadata/IActionCore.ts index ffdf8ea9..8ae8f349 100644 --- a/packages/questionable-core/src/survey/IActionCore.ts +++ b/packages/questionable-core/src/metadata/IActionCore.ts @@ -1,6 +1,6 @@ -import { ACTION } from '../util/enums'; import { IButtonCore } from './IButtonCore'; import { IRefCore } from './IRefCore'; +import { TActionType } from './properties/type/TActionType'; /** * Represents something the customer can do in response to receiving a result @@ -11,11 +11,11 @@ export interface IActionCore extends IRefCore { * @title Buttons * @hidden */ - buttons: IButtonCore[]; + buttons?: IButtonCore[]; /** * @title Label */ - label: string; + label?: string; /** * @title Description */ @@ -24,5 +24,5 @@ export interface IActionCore extends IRefCore { * @title Type * @hidden */ - type: ACTION; + type: TActionType; } diff --git a/packages/questionable-core/src/metadata/IAnswerCore.ts b/packages/questionable-core/src/metadata/IAnswerCore.ts new file mode 100644 index 00000000..3c32e27a --- /dev/null +++ b/packages/questionable-core/src/metadata/IAnswerCore.ts @@ -0,0 +1,8 @@ +import { IRefCore } from './IRefCore'; +import { TAnswerType } from './properties/type/TAnswerType'; + +export interface IAnswerCore extends IRefCore { + key?: string; + synonyms?: string[]; + type?: TAnswerType; +} diff --git a/packages/questionable-core/src/metadata/IBranchCore.ts b/packages/questionable-core/src/metadata/IBranchCore.ts new file mode 100644 index 00000000..6fa1d2be --- /dev/null +++ b/packages/questionable-core/src/metadata/IBranchCore.ts @@ -0,0 +1,10 @@ +import { IQuestionCore } from './IQuestionCore'; +import { IRefCore } from './IRefCore'; +import { ISectionCore } from './ISectionCore'; +import { TBranchType } from './properties/type/TBranchType'; + +export interface IBranchCore extends IRefCore { + questions?: IQuestionCore[]; + sections?: ISectionCore[]; + type?: TBranchType; +} diff --git a/packages/questionable-core/src/survey/IButtonCore.ts b/packages/questionable-core/src/metadata/IButtonCore.ts similarity index 73% rename from packages/questionable-core/src/survey/IButtonCore.ts rename to packages/questionable-core/src/metadata/IButtonCore.ts index 1839b451..d502e3db 100644 --- a/packages/questionable-core/src/survey/IButtonCore.ts +++ b/packages/questionable-core/src/metadata/IButtonCore.ts @@ -1,7 +1,5 @@ -import { - TButtonModeCore, -} from '../util/types'; -import { IRefCore } from './IRefCore'; +import { IRefCore } from './IRefCore'; +import { TButtonType } from './properties/type/TButtonType'; /** * Represents a navigation button @@ -18,7 +16,7 @@ export interface IButtonCore extends IRefCore { * * @title Mode */ - type?: TButtonModeCore; + type?: TButtonType; /** * Visibility status of the button (show/hide) * diff --git a/packages/questionable-core/src/metadata/IConfigCore.ts b/packages/questionable-core/src/metadata/IConfigCore.ts new file mode 100644 index 00000000..5bb77219 --- /dev/null +++ b/packages/questionable-core/src/metadata/IConfigCore.ts @@ -0,0 +1,76 @@ +import { MODE } from '../lib/enums'; +import { TGetDictionaryCore, TStringDictionaryCore } from './types/TStringDictionaryCore'; +import { IEventCore } from './IEventCore'; +import { TVisible } from './types/TVisible'; + +/** + * Configuration for customized behavior of Questionable + */ +export interface IQuestionableConfigCore { + /** + * Enables all developer tools (NOT for production use!) + * + * @title Developer Mode + * @default false + * @hidden + */ + readonly dev?: boolean; + /** + * Event hooks for common form operations + * + * @title Events + * @hidden + */ + events?: IEventCore; + /** + * Optional method to fetch environment variables or query string parameters + * + * @title Get Runtime Config + * @hidden + */ + getRuntimeConfig?: TGetDictionaryCore; + /** + * View or edit mode + * + * @title Mode + * @default MODE.VIEW + */ + mode?: MODE; + /** + * Navigation configuration + * + * @title Navigation + */ + nav?: TVisible; + /** + * Page configuration + * + * @title Pages + */ + pages?: TVisible; + /** + * Properties produced from `getRuntimeConfig()` + * @title Params + * @default {} + */ + params?: TStringDictionaryCore; + /** + * Progress Bar configuration + * + * @title Progress Bar + */ + progressBar?: TVisible; + /** + * Question configuration + * + * @title Question Configuration + */ + questions?: TVisible; + + /** + * Step configuration + * + * @title Step Configuration + */ + steps?: TVisible; +} diff --git a/packages/questionable-core/src/metadata/IEventCore.ts b/packages/questionable-core/src/metadata/IEventCore.ts new file mode 100644 index 00000000..cd4131eb --- /dev/null +++ b/packages/questionable-core/src/metadata/IEventCore.ts @@ -0,0 +1,18 @@ +import { TOnEventCore, TOnErrorCore } from './types/TGateCore'; + +/** + * Event Model + * @title Event + */ +export interface IEventCore { + readonly onActionClick?: TOnEventCore; + readonly onAnswer?: TOnEventCore; + readonly onAnyEvent?: TOnEventCore; + readonly onBranch?: TOnEventCore; + readonly onError?: TOnErrorCore; + readonly onGateSwitch?: TOnEventCore; + readonly onInit?: TOnEventCore; + readonly onNoResults?: TOnEventCore; + readonly onPage?: TOnEventCore; + readonly onResults?: TOnEventCore; +} diff --git a/packages/questionable-core/src/survey/IFormCore.ts b/packages/questionable-core/src/metadata/IFormCore.ts similarity index 77% rename from packages/questionable-core/src/survey/IFormCore.ts rename to packages/questionable-core/src/metadata/IFormCore.ts index 46418cb2..ff722d9c 100644 --- a/packages/questionable-core/src/survey/IFormCore.ts +++ b/packages/questionable-core/src/metadata/IFormCore.ts @@ -1,5 +1,5 @@ -import { TAgeCore } from '../util/types'; -import { IQuestionCore } from './IStepCore'; +import { TAgeCore } from './types/TAgeCore'; +import { IQuestionCore } from './IQuestionCore'; /** * Represents the survey as completed by the user @@ -28,11 +28,11 @@ export interface IFormCore { * * @title Responses */ - responses: IQuestionCore[]; + responses?: IQuestionCore[]; /** * Time the survey was started * * @title Started */ - readonly started: Date; + started: Date; } diff --git a/packages/questionable-core/src/metadata/IPageCore.ts b/packages/questionable-core/src/metadata/IPageCore.ts new file mode 100644 index 00000000..35ef61c8 --- /dev/null +++ b/packages/questionable-core/src/metadata/IPageCore.ts @@ -0,0 +1,33 @@ +import { IStepCore } from './IStepCore'; +import { TPageType } from './properties/type/TPageType'; + +/** + * Defines step content for Page types + */ +export interface IPageCore extends IStepCore { + /** + * Defines the body content of the page + * + * @title Body + */ + body?: string; + /** + * Optional header to display above body + * + * @title Body Heading + */ + bodyHeader?: string; + /** + * Optional sub header to display below Body Heading + * + * @title Body Subheading + */ + bodySubHeader?: string; + display: boolean; + /** + * Type of page + * + * @title Page Type + */ + type: TPageType; +} diff --git a/packages/questionable-core/src/survey/IPagesCore.ts b/packages/questionable-core/src/metadata/IPagesCore.ts similarity index 76% rename from packages/questionable-core/src/survey/IPagesCore.ts rename to packages/questionable-core/src/metadata/IPagesCore.ts index 050e597f..18812cf8 100644 --- a/packages/questionable-core/src/survey/IPagesCore.ts +++ b/packages/questionable-core/src/metadata/IPagesCore.ts @@ -1,4 +1,4 @@ -import { IPageCore } from './IStepCore'; +import { IPageCore } from './IPageCore'; /** * Defines required pages for the survey flow @@ -15,17 +15,18 @@ export interface IPagesCore { * * @title No Results Page */ - noResultsPage: IPageCore; + noResultsPage?: IPageCore; + pages?: IPageCore[]; /** * Last step of the survey if there are 1 or more results * * @title Results Page */ - resultsPage: IPageCore; + resultsPage?: IPageCore; /** * Preview of survery before submitting to receive results * * @title Summary Page */ - summaryPage: IPageCore; + summaryPage?: IPageCore; } diff --git a/packages/questionable-core/src/metadata/IQuestionCore.ts b/packages/questionable-core/src/metadata/IQuestionCore.ts new file mode 100644 index 00000000..602b5f9f --- /dev/null +++ b/packages/questionable-core/src/metadata/IQuestionCore.ts @@ -0,0 +1,36 @@ +import { IAnswerCore } from './IAnswerCore'; +import { IBranchCore } from './IBranchCore'; +import { IStepCore } from './IStepCore'; +import { TQuestionType } from './properties/type/TQuestionType'; + +/** + * Defines step content for Question type + */ +export interface IQuestionCore extends IStepCore { + /** + * The current answer for this question + * + * @title Answer + * @hidden Not viewable/editable in Design Mode + */ + answer?: string; + /** + * Collection of allowed answers + * + * @title Answers + */ + answers: IAnswerCore[]; + /** + * Collection of branches that use this question + * + * @title Branch + * @hidden + */ + branch?: IBranchCore; + /** + * Type of question + * + * @title Question Type + */ + type: TQuestionType; +} diff --git a/packages/questionable-core/src/metadata/IQuestionnaireCore.ts b/packages/questionable-core/src/metadata/IQuestionnaireCore.ts new file mode 100644 index 00000000..46ab51b9 --- /dev/null +++ b/packages/questionable-core/src/metadata/IQuestionnaireCore.ts @@ -0,0 +1,21 @@ +import { IActionCore } from './IActionCore'; +import { IBranchCore } from './IBranchCore'; +import { IPagesCore } from './IPagesCore'; +import { IQuestionableConfigCore } from './IConfigCore'; +import { IResultCore } from './IResultCore'; +import { IQuestionCore } from './IQuestionCore'; +import { ISectionCore } from './ISectionCore'; + +/** + * Definition for survey data input + */ +export interface IQuestionnaireCore { + actions?: IActionCore[]; + branches?: IBranchCore[]; + config?: IQuestionableConfigCore; + header?: string; + pages: IPagesCore; + questions: IQuestionCore[]; + results?: IResultCore[]; + sections?: ISectionCore[]; +} diff --git a/packages/questionable-core/src/survey/IRefCore.ts b/packages/questionable-core/src/metadata/IRefCore.ts similarity index 61% rename from packages/questionable-core/src/survey/IRefCore.ts rename to packages/questionable-core/src/metadata/IRefCore.ts index 3e5378c1..6dbc9e96 100644 --- a/packages/questionable-core/src/survey/IRefCore.ts +++ b/packages/questionable-core/src/metadata/IRefCore.ts @@ -1,3 +1,5 @@ +import { TInstanceOf } from '../lib/instanceOf'; + /** Generic reference object */ export interface IRefCore { /** @@ -5,7 +7,13 @@ export interface IRefCore { * * @title Id */ - id: string; + id?: string; + instanceOfCheck: TInstanceOf; + /** + * @title Optional label + * @hidden + */ + label?: string; /** * Optional order * @@ -16,7 +24,7 @@ export interface IRefCore { /** * @title Title */ - title?: string; + title: string; /** * @title Type * @hidden diff --git a/packages/questionable-core/src/metadata/IRequirementCore.ts b/packages/questionable-core/src/metadata/IRequirementCore.ts new file mode 100644 index 00000000..55343c6c --- /dev/null +++ b/packages/questionable-core/src/metadata/IRequirementCore.ts @@ -0,0 +1,42 @@ +import { TAgeCalcCore } from '../lib/types'; +import { TAgeCore } from './types/TAgeCore'; +import { IRefCore } from './IRefCore'; +import { IResponseCore } from './IResponseCore'; +import { TRequirementType } from './properties/type/TRequirementType'; + +/** + * Defines an individual requirement for accessing a step + */ +export interface IRequirementCore extends IRefCore { + /** + * Optional, custom calculator for performing age-specific validation + * @hidden JSON schema does not support functions + */ + ageCalc?: TAgeCalcCore; + /** + * User facing description of this requirement + * + * @title Exlanation + */ + explanation: string; + /** + * Optional maximum age allowed for this requirement + * + * @title Maximum Age + */ + maxAge?: TAgeCore; + /** + * Optional minimum age allowed for this requirement + * + * @title Minimum Age + */ + minAge?: TAgeCore; + /** + * Map of step id to required answer values + * + * @title Answers + */ + responses: IResponseCore[]; + + type?: TRequirementType; +} diff --git a/packages/questionable-core/src/metadata/IResponseCore.ts b/packages/questionable-core/src/metadata/IResponseCore.ts new file mode 100644 index 00000000..6dc03bd3 --- /dev/null +++ b/packages/questionable-core/src/metadata/IResponseCore.ts @@ -0,0 +1,13 @@ +import { IAnswerCore } from './IAnswerCore'; +import { IQuestionCore } from './IQuestionCore'; +import { IRefCore } from './IRefCore'; +import { TResponseType } from './properties/type/TResponseType'; + +/** + * Acceptable responses + */ +export interface IResponseCore extends IRefCore { + answers: IAnswerCore[]; + question?: IQuestionCore; + type?: TResponseType; +} diff --git a/packages/questionable-core/src/survey/IResultCore.ts b/packages/questionable-core/src/metadata/IResultCore.ts similarity index 80% rename from packages/questionable-core/src/survey/IResultCore.ts rename to packages/questionable-core/src/metadata/IResultCore.ts index ef384c7d..fa656557 100644 --- a/packages/questionable-core/src/survey/IResultCore.ts +++ b/packages/questionable-core/src/metadata/IResultCore.ts @@ -1,6 +1,7 @@ import { IActionCore } from './IActionCore'; import { IRefCore } from './IRefCore'; -import { IRequirementCore } from './IStepCore'; +import { IRequirementCore } from './IRequirementCore'; +import { TResultType } from './properties/type/TResultType'; /** * Represents a potential result based on a customer's answers @@ -12,7 +13,7 @@ export interface IResultCore extends IRefCore { * @title Call to Action * @hidden */ - action: Partial; + action?: IActionCore; /** * Optional tag/category to group results */ @@ -22,7 +23,7 @@ export interface IResultCore extends IRefCore { * * @title Label */ - label: string; + label?: string; /** * Requirement used for applying this result * Could have more than one, we only store the first @@ -42,12 +43,14 @@ export interface IResultCore extends IRefCore { * * @title Requirements */ - requirements: IRequirementCore[]; + requirements?: IRequirementCore[]; /** * Additional action which may follow after the primary * * @title Secondary Action * @hidden */ - secondaryAction?: Partial; + secondaryAction?: IActionCore; + + type?: TResultType; } diff --git a/packages/questionable-core/src/metadata/ISectionCore.ts b/packages/questionable-core/src/metadata/ISectionCore.ts new file mode 100644 index 00000000..043f62cd --- /dev/null +++ b/packages/questionable-core/src/metadata/ISectionCore.ts @@ -0,0 +1,32 @@ +import { IRefCore } from './IRefCore'; +import { IRequirementCore } from './IRequirementCore'; +import { TProgressBarStatusType } from './types/TProgressBarStatusType'; +import { TSectionType } from './properties/type/TSectionType'; + +/** + * Defines a survey section, used in progress bar + */ +export interface ISectionCore extends IRefCore { + /** + * The last step id that is covered by this section + * + * @title Last Step + * @hidden Not viewable/editable in Design Mode + */ + lastStep?: number; + /** + * Collection of requirements to enable display of this status + * + * @title Requirements + */ + requirements?: IRequirementCore[]; + /** + * Current display status of this section + * + * @title Status + * @hidden Not viewable/editable in Design Mode + */ + status?: TProgressBarStatusType; + + type?: TSectionType; +} diff --git a/packages/questionable-core/src/metadata/IStepCore.ts b/packages/questionable-core/src/metadata/IStepCore.ts new file mode 100644 index 00000000..33e40aa9 --- /dev/null +++ b/packages/questionable-core/src/metadata/IStepCore.ts @@ -0,0 +1,66 @@ +/* eslint-disable import/no-cycle */ +import { IRefCore } from './IRefCore'; +import { IRequirementCore } from './IRequirementCore'; +import { ISectionCore } from './ISectionCore'; +import { TStepType } from './properties/type/TStepType'; + +/** + * Generic step data definition. Applies to all types of steps. + */ +export interface IStepCore extends IRefCore { + /** + * Collection of requirements to view/enter this step + * + * @title Requirements + */ + entryRequirements?: IRequirementCore[]; + /** + * Collection of requirements to leave this step + * + * @title Exit Requirements + */ + exitRequirements?: IRequirementCore[]; + /** + * Optional footer text to display at the bottom of the step + * + * @title Footer + */ + footer?: string; + /** + * Contextual content to display below the step contents and above the footer + * + * @title Info + */ + info?: string; + /** + * Private/internal use only notes for documenting this step + * + * @title Internal Notes + */ + internalNotes?: string; + /** + * Display order of the Step. Determined at runtime. + * + * @title Order + * @hidden + */ + order?: number; + /** + * Section to which this step belongs + * + * @title Section + */ + section?: ISectionCore; + /** + * Text to display below the title + * + * @title Subtitle + */ + subTitle?: string; + /** + * Step's type, usually implemented by @see{IPageStep} or @see{IQuestionStep} + * + * @title Step Type + */ + type: TStepType; +} diff --git a/packages/questionable-core/src/survey/README.md b/packages/questionable-core/src/metadata/README.md similarity index 85% rename from packages/questionable-core/src/survey/README.md rename to packages/questionable-core/src/metadata/README.md index 3dc53383..018fa583 100644 --- a/packages/questionable-core/src/survey/README.md +++ b/packages/questionable-core/src/metadata/README.md @@ -2,7 +2,7 @@ Interfaces that define the schema. -Requirements for inclusion in this (the `/survey`) folder: +Requirements for inclusion in this (the `/metadata`) folder: - [ ] File includes only interfaces - [ ] All interfaces are exported (public) diff --git a/packages/questionable-core/src/survey/index.ts b/packages/questionable-core/src/metadata/_exports.ts similarity index 50% rename from packages/questionable-core/src/survey/index.ts rename to packages/questionable-core/src/metadata/_exports.ts index 5d2c6b65..e626553a 100644 --- a/packages/questionable-core/src/survey/index.ts +++ b/packages/questionable-core/src/metadata/_exports.ts @@ -1,15 +1,19 @@ export * from './IActionCore'; +export * from './IAnswerCore'; export * from './IBranchCore'; export * from './IButtonCore'; -export * from './IDesignDataCore'; +export * from './IConfigCore'; export * from './IEventCore'; export * from './IFormCore'; -export * from './IPageDataCore'; +export * from './IPageCore'; export * from './IPagesCore'; -export * from './IQuestionDataCore'; -export * from './IQuestionableConfigCore'; +export * from './IQuestionCore'; export * from './IQuestionnaireCore'; export * from './IRefCore'; +export * from './IRequirementCore'; +export * from './IResponseCore'; export * from './IResultCore'; +export * from './ISectionCore'; export * from './IStepCore'; -export * from './IStepDataCore'; +export * from './properties/_exports'; +export * from './types/_exports'; diff --git a/packages/questionable-core/src/metadata/properties/MAction.ts b/packages/questionable-core/src/metadata/properties/MAction.ts new file mode 100644 index 00000000..37650444 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MAction.ts @@ -0,0 +1,20 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ERefCoreProperties, TRefCoreProperties } from './MRef'; + +const TheseProperties: { + readonly buttons: 'buttons', + readonly subTitle: 'subTitle', +} = { + buttons: 'buttons' as const, + subTitle: 'subTitle' as const, +}; + +const EActionCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TActionCoreProperties = ClassProperties | TRefCoreProperties; +type TActionPrivateProps = `_${TActionCoreProperties}`; + +export { + EActionCoreProperties, + type TActionCoreProperties, + type TActionPrivateProps, +}; diff --git a/packages/questionable-core/src/metadata/properties/MAnswer.ts b/packages/questionable-core/src/metadata/properties/MAnswer.ts new file mode 100644 index 00000000..8898aae8 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MAnswer.ts @@ -0,0 +1,16 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { + ERefCoreProperties, + TRefCoreProperties, +} from './MRef'; + +type TTheseProperties = typeof ERefCoreProperties; +const TheseProperties: TTheseProperties = { + ...ERefCoreProperties, +}; +type TAnswerCoreProperties = ClassProperties | TRefCoreProperties; + +export { + TheseProperties as EAnswerCoreProperties, + type TAnswerCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MBase.ts b/packages/questionable-core/src/metadata/properties/MBase.ts new file mode 100644 index 00000000..9e54b675 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MBase.ts @@ -0,0 +1,11 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ERefCoreProperties } from './MRef'; + +const TheseProperties = {}; +const EBaseCoreProperties = { ...ERefCoreProperties, ...TheseProperties }; +type TBaseCoreProperties = ClassProperties; + +export { + EBaseCoreProperties, + type TBaseCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MBranch.ts b/packages/questionable-core/src/metadata/properties/MBranch.ts new file mode 100644 index 00000000..7ab68cb2 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MBranch.ts @@ -0,0 +1,19 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { + ERefCoreProperties, + TRefCoreProperties, +} from './MRef'; + +const TheseProperties: { + readonly questions: 'questions', +} = { + questions: 'questions' as const, +}; + +const EBranchCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TBranchCoreProperties = ClassProperties | TRefCoreProperties; + +export { + EBranchCoreProperties, + type TBranchCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MCommon.ts b/packages/questionable-core/src/metadata/properties/MCommon.ts new file mode 100644 index 00000000..3ba10eb1 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MCommon.ts @@ -0,0 +1,14 @@ +import { ClassProperties } from '../types/ClassProperties'; + +const TheseProperties: { + readonly instanceOfCheck: 'instanceOfCheck', +} = { + instanceOfCheck: 'instanceOfCheck' as const, +}; + +type TCommonCoreProperties = ClassProperties; + +export { + TheseProperties as ECommonCoreProperties, + type TCommonCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MConfig.ts b/packages/questionable-core/src/metadata/properties/MConfig.ts new file mode 100644 index 00000000..4b5f2c28 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MConfig.ts @@ -0,0 +1,34 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +type TTheseProperties = { + dev: 'dev'; + events: 'events'; + getRuntimeConfig: 'getRuntimeConfig'; + mode: 'mode'; + nav: 'nav'; + pages: 'pages'; + params: 'params'; + progressBar: 'progressBar'; + questions: 'questions'; + steps: 'steps'; +}; +const TheseProperties: TTheseProperties = { + dev: 'dev' as const, + events: 'events' as const, + getRuntimeConfig: 'getRuntimeConfig' as const, + mode: 'mode' as const, + nav: 'nav' as const, + pages: 'pages' as const, + params: 'params' as const, + progressBar: 'progressBar' as const, + questions: 'questions' as const, + steps: 'steps' as const, +}; +const EConfigCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TConfigCoreProperties = ClassProperties; + +export { + EConfigCoreProperties, + type TConfigCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MEvent.ts b/packages/questionable-core/src/metadata/properties/MEvent.ts new file mode 100644 index 00000000..99de84a8 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MEvent.ts @@ -0,0 +1,33 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +type TTheseProperties = { + readonly onActionClick: 'onActionClick', + readonly onAnswer: 'onAnswer', + readonly onAnyEvent: 'onAnyEvent', + readonly onBranch: 'onBranch', + readonly onError: 'onError', + readonly onGateSwitch: 'onGateSwitch', + readonly onInit: 'onInit', + readonly onNoResults: 'onNoResults', + readonly onPage: 'onPage', + readonly onResults: 'onResults' +}; +const TheseProperties: TTheseProperties = { + onActionClick: 'onActionClick' as const, + onAnswer: 'onAnswer' as const, + onAnyEvent: 'onAnyEvent' as const, + onBranch: 'onBranch' as const, + onError: 'onError' as const, + onGateSwitch: 'onGateSwitch' as const, + onInit: 'onInit' as const, + onNoResults: 'onNoResults' as const, + onPage: 'onPage' as const, + onResults: 'onResults', +}; +const EEventCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TEventCoreProperties = ClassProperties; +export { + EEventCoreProperties, + type TEventCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MForm.ts b/packages/questionable-core/src/metadata/properties/MForm.ts new file mode 100644 index 00000000..f748bc95 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MForm.ts @@ -0,0 +1,24 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +type TTheseProperties = { + readonly age: 'age', + readonly birthdate: 'birthdate', + readonly finished: 'finished', + readonly responses: 'responses', + readonly started: 'started' +}; +const TheseProperties: TTheseProperties = { + age: 'age' as const, + birthdate: 'birthdate' as const, + finished: 'finished' as const, + responses: 'responses' as const, + started: 'started' as const, +}; +const EFormCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TFormCoreProperties = ClassProperties; + +export { + EFormCoreProperties, + type TFormCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MPage.ts b/packages/questionable-core/src/metadata/properties/MPage.ts new file mode 100644 index 00000000..b1220642 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MPage.ts @@ -0,0 +1,22 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { EStepCoreProperties, TStepCoreProperties } from './MStep'; + +type TTheseProperties = { + readonly body: 'body'; + readonly bodyHeader: 'bodyHeader'; + readonly bodySubHeader: 'bodySubHeader'; + readonly type: 'type'; +}; +const TheseProperties: TTheseProperties = { + body: 'body', + bodyHeader: 'bodyHeader', + bodySubHeader: 'bodySubHeader', + type: 'type', +}; +const EPageCoreProperties = { ...TheseProperties, ...EStepCoreProperties }; +type TPageCoreProperties = ClassProperties | TStepCoreProperties; + +export { + EPageCoreProperties, + type TPageCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MPages.ts b/packages/questionable-core/src/metadata/properties/MPages.ts new file mode 100644 index 00000000..d162690c --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MPages.ts @@ -0,0 +1,22 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +type TTheseProperties = { + readonly landingPage: 'landingPage'; + readonly noResultsPage: 'noResultsPage'; + readonly resultsPage: 'resultsPage'; + readonly summaryPage: 'summaryPage'; +}; +const TheseProperties: TTheseProperties = { + landingPage: 'landingPage' as const, + noResultsPage: 'noResultsPage' as const, + resultsPage: 'resultsPage' as const, + summaryPage: 'summaryPage' as const, +}; +const EPagesCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TEPagesCoreProperties = ClassProperties; + +export { + EPagesCoreProperties, + type TEPagesCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MQuestion.ts b/packages/questionable-core/src/metadata/properties/MQuestion.ts new file mode 100644 index 00000000..2d503e23 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MQuestion.ts @@ -0,0 +1,23 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { EStepCoreProperties, TStepCoreProperties } from './MStep'; + +type TTheseProperties = { + readonly answer: 'answer'; + readonly answers: 'answers'; + readonly branch: 'branch'; + readonly type: 'type'; +}; +const TheseProperties: TTheseProperties = { + answer: 'answer' as const, + answers: 'answers' as const, + branch: 'branch' as const, + type: 'type' as const, +}; + +const EQuestionCoreProperties = { ...TheseProperties, ...EStepCoreProperties }; +type TQuestionCoreProperties = ClassProperties | TStepCoreProperties; + +export { + EQuestionCoreProperties, + type TQuestionCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MQuestionnaire.ts b/packages/questionable-core/src/metadata/properties/MQuestionnaire.ts new file mode 100644 index 00000000..09f4fde4 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MQuestionnaire.ts @@ -0,0 +1,34 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +type TTheseProperties = { + readonly actions: 'actions'; + readonly branches: 'branches'; + readonly config: 'config'; + readonly flow: 'flow'; + readonly form: 'form'; + readonly header: 'header'; + readonly pages: 'pages'; + readonly questions: 'questions'; + readonly results: 'results'; + readonly sections: 'sections'; +}; +const TheseProperties: TTheseProperties = { + actions: 'actions' as const, + branches: 'branches' as const, + config: 'config' as const, + flow: 'flow' as const, + form: 'form' as const, + header: 'header' as const, + pages: 'pages' as const, + questions: 'questions' as const, + results: 'results' as const, + sections: 'sections' as const, +}; +const EQuestionnaireCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TQuestionnaireCoreProperties = ClassProperties; + +export { + EQuestionnaireCoreProperties, + type TQuestionnaireCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MRef.ts b/packages/questionable-core/src/metadata/properties/MRef.ts new file mode 100644 index 00000000..14d57e50 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MRef.ts @@ -0,0 +1,24 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ECommonCoreProperties } from './MCommon'; + +const TheseProperties: { + readonly id: 'id', + readonly label: 'label', + readonly order: 'order', + readonly title: 'title', + readonly type: 'type' +} = { + id: 'id' as const, + label: 'label' as const, + order: 'order' as const, + title: 'title' as const, + type: 'type' as const, +}; +const ERefCoreProperties = { ...ECommonCoreProperties, ...TheseProperties }; +type TRefCoreProperties = ClassProperties; +type TRefCorePrivateProps = `_${TRefCoreProperties}`; +export { + ERefCoreProperties, + type TRefCoreProperties, + type TRefCorePrivateProps, +}; diff --git a/packages/questionable-core/src/metadata/properties/MRequirement.ts b/packages/questionable-core/src/metadata/properties/MRequirement.ts new file mode 100644 index 00000000..62045d7c --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MRequirement.ts @@ -0,0 +1,23 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ERefCoreProperties } from './MRef'; + +const TheseProperties: { + readonly ageCalc: 'ageCalc'; + readonly explanation: 'explanation'; + readonly maxAge: 'maxAge'; + readonly minAge: 'minAge'; + readonly responses: 'responses'; +} = { + ageCalc: 'ageCalc' as const, + explanation: 'explanation' as const, + maxAge: 'maxAge' as const, + minAge: 'minAge' as const, + responses: 'responses' as const, +}; +const ERequirementCoreProperties = { ...ERefCoreProperties, ...TheseProperties }; +type TRequirementCoreProperties = ClassProperties; + +export { + ERequirementCoreProperties, + type TRequirementCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MResponse.ts b/packages/questionable-core/src/metadata/properties/MResponse.ts new file mode 100644 index 00000000..5d128cf9 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MResponse.ts @@ -0,0 +1,18 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ERefCoreProperties, TRefCoreProperties } from './MRef'; + +type TTheseProperties = { + readonly answers: 'answers', + readonly question: 'question', +}; +const TheseProperties: TTheseProperties = { + answers: 'answers' as const, + question: 'question' as const, +}; +const EResponseCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TResponseCoreProperties = ClassProperties | TRefCoreProperties; + +export { + EResponseCoreProperties, + type TResponseCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MResult.ts b/packages/questionable-core/src/metadata/properties/MResult.ts new file mode 100644 index 00000000..6d230a4b --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MResult.ts @@ -0,0 +1,31 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { + ERefCoreProperties, + TRefCoreProperties, +} from './MRef'; + +type TTheseProperties = { + readonly action: 'action'; + readonly category: 'category'; + readonly label: 'label'; + readonly match: 'match'; + readonly reason: 'reason'; + readonly requirements: 'requirements'; + readonly secondaryAction: 'secondaryAction'; +} +const TheseProperties: TTheseProperties = { + action: 'action' as const, + category: 'category' as const, + label: 'label' as const, + match: 'match' as const, + reason: 'reason' as const, + requirements: 'requirements' as const, + secondaryAction: 'secondaryAction' as const, +}; +const EResultCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TResultCoreProperties = ClassProperties | TRefCoreProperties; + +export { + EResultCoreProperties, + type TResultCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MSection.ts b/packages/questionable-core/src/metadata/properties/MSection.ts new file mode 100644 index 00000000..f808f907 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MSection.ts @@ -0,0 +1,23 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { + ERefCoreProperties, + TRefCoreProperties, +} from './MRef'; + +type TTheseProperties = { + readonly lastStep: 'lastStep'; + readonly requirements: 'requirements'; + readonly status: 'status'; +}; +const TheseProperties: TTheseProperties = { + lastStep: 'lastStep' as const, + requirements: 'requirements' as const, + status: 'status' as const, +}; +const ESectionCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TSectionCoreProperties = ClassProperties | TRefCoreProperties; + +export { + ESectionCoreProperties, + type TSectionCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/MStep.ts b/packages/questionable-core/src/metadata/properties/MStep.ts new file mode 100644 index 00000000..84777dc3 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/MStep.ts @@ -0,0 +1,31 @@ +import { ClassProperties } from '../types/ClassProperties'; +import { ERefCoreProperties, TRefCoreProperties } from './MRef'; + +type TTheseProperties = { + readonly entryRequirements: 'entryRequirements', + readonly exitRequirements: 'exitRequirements', + readonly footer: 'footer', + readonly info: 'info', + readonly internalNotes: 'internalNotes', + readonly order: 'order', + readonly section: 'section', + readonly subTitle: 'subTitle', +}; +const TheseProperties: TTheseProperties = { + entryRequirements: 'entryRequirements' as const, + exitRequirements: 'exitRequirements' as const, + footer: 'footer' as const, + info: 'info' as const, + internalNotes: 'internalNotes' as const, + order: 'order' as const, + section: 'section' as const, + subTitle: 'subTitle' as const, +}; + +const EStepCoreProperties = { ...TheseProperties, ...ERefCoreProperties }; +type TStepCoreProperties = ClassProperties | TRefCoreProperties; + +export { + EStepCoreProperties, + type TStepCoreProperties, +}; diff --git a/packages/questionable-core/src/metadata/properties/README.md b/packages/questionable-core/src/metadata/properties/README.md new file mode 100644 index 00000000..108219a7 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/README.md @@ -0,0 +1,8 @@ +# Metadata + +Reflection in Typscript is tenuous at times. This is an effort to type the names of a type's properties in order that they can be referenced dynamically at runtime while still providing compile time consistency. + +For example, we may know in composition that `class Foo` has property `bar`, and we can reference it by index (e.g. `let x = fooInstance['bar']`) but it becomes non-trivial to allow an arbitrary external consumer to say `let y = fooInstance[someStringKey]` without either strongly typed metadata or reflection. + +This provides the former, but reflection would be preferable if and when it becomes available in TypeScript without using heavily customized implementations of the transpiler. + diff --git a/packages/questionable-core/src/metadata/properties/_exports.ts b/packages/questionable-core/src/metadata/properties/_exports.ts new file mode 100644 index 00000000..10daac8c --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/_exports.ts @@ -0,0 +1,19 @@ +export * from './MAction'; +export * from './MAnswer'; +export * from './MBase'; +export * from './MBranch'; +export * from './MCommon'; +export * from './MConfig'; +export * from './MEvent'; +export * from './MForm'; +export * from './MPage'; +export * from './MPages'; +export * from './MResponse'; +export * from './MQuestion'; +export * from './MQuestionnaire'; +export * from './MRef'; +export * from './MRequirement'; +export * from './MResult'; +export * from './MSection'; +export * from './MStep'; +export * from './type/_exports'; diff --git a/packages/questionable-core/src/metadata/properties/type/TActionType.ts b/packages/questionable-core/src/metadata/properties/type/TActionType.ts new file mode 100644 index 00000000..e41903ee --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TActionType.ts @@ -0,0 +1,18 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TActionType = 'call' | 'hybrid' | 'none' | 'online' | 'shell'; +type TEnmActionType = TEnmBaseType & { + CALL: TActionType & 'call'; + HYBRID: TActionType & 'hybrid'; + NONE: TActionType & 'none'; + ONLINE: TActionType & 'online'; + SHELL: TActionType & 'shell'; +}; +export const ACTION_TYPE: TEnmActionType = { + ...BASE_TYPE, + CALL: 'call', + HYBRID: 'hybrid', + NONE: 'none', + ONLINE: 'online', + SHELL: 'shell', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TAnswerType.ts b/packages/questionable-core/src/metadata/properties/type/TAnswerType.ts new file mode 100644 index 00000000..406ddf11 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TAnswerType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TAnswerType = 'fixed' | 'variable'; +type TEnmAnswerType = TEnmBaseType & { + FIXED: TAnswerType & 'fixed'; + VARIABLE: TAnswerType & 'variable'; +}; +export const ANSWER_TYPE: TEnmAnswerType = { + ...BASE_TYPE, + FIXED: 'fixed', + VARIABLE: 'variable', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TBaseType.ts b/packages/questionable-core/src/metadata/properties/type/TBaseType.ts new file mode 100644 index 00000000..efb6da5c --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TBaseType.ts @@ -0,0 +1,4 @@ +export type TBaseType = 'default'; +export const DEFAULT_TYPE: TBaseType = 'default'; +export type TEnmBaseType = { DEFAULT: TBaseType; }; +export const BASE_TYPE: TEnmBaseType = { DEFAULT: DEFAULT_TYPE }; diff --git a/packages/questionable-core/src/metadata/properties/type/TBranchType.ts b/packages/questionable-core/src/metadata/properties/type/TBranchType.ts new file mode 100644 index 00000000..48c978e5 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TBranchType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TBranchType = 'linear' | 'non-linear'; +type TEnmBranchType = TEnmBaseType & { + LINEAR: TBranchType & 'linear'; + NON_LINEAR: TBranchType & 'non-linear'; +}; +export const BRANCH_TYPE: TEnmBranchType = { + ...BASE_TYPE, + LINEAR: 'linear', + NON_LINEAR: 'non-linear', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TButtonType.ts b/packages/questionable-core/src/metadata/properties/type/TButtonType.ts new file mode 100644 index 00000000..4c90c7fa --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TButtonType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TButtonType = 'button' | 'link'; +type TEnmBranchType = TEnmBaseType & { + BUTTON: TButtonType & 'button'; + LINK: TButtonType & 'link'; +}; +export const BUTTON_TYPE: TEnmBranchType = { + ...BASE_TYPE, + BUTTON: 'button', + LINK: 'link', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TPageType.ts b/packages/questionable-core/src/metadata/properties/type/TPageType.ts new file mode 100644 index 00000000..ab9d542d --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TPageType.ts @@ -0,0 +1,20 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +/** + * Defines the known component types for pages + */ + +export type TPageType = 'Landing' | 'No Results' | 'Results' | 'Summary'; +type TEnmPageType = TEnmBaseType & { + LANDING: TPageType & 'Landing'; + NO_RESULTS: TPageType & 'No Results'; + RESULTS: TPageType & 'Results'; + SUMMARY: TPageType & 'Summary'; +}; +export const PAGE_TYPE: TEnmPageType = { + ...BASE_TYPE, + LANDING: 'Landing', + NO_RESULTS: 'No Results', + RESULTS: 'Results', + SUMMARY: 'Summary', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TQuestionType.ts b/packages/questionable-core/src/metadata/properties/type/TQuestionType.ts new file mode 100644 index 00000000..33c53d3c --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TQuestionType.ts @@ -0,0 +1,24 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +/** + * Defines the known component types for questions + */ + +export type TQuestionType = 'date_time' | 'dob' | 'multiple_choice' | 'multiple_select' | 'path' | 'text'; +type TEnmQuestionType = TEnmBaseType & { + DATE_TIME: TQuestionType & 'date_time'; + DOB: TQuestionType & 'dob'; + MULTIPLE_CHOICE: TQuestionType & 'multiple_choice'; + MULTIPLE_SELECT: TQuestionType & 'multiple_select'; + PATH: TQuestionType & 'path'; + TEXT: TQuestionType & 'text'; +}; +export const QUESTION_TYPE: TEnmQuestionType = { + ...BASE_TYPE, + DATE_TIME: 'date_time', + DOB: 'dob', + MULTIPLE_CHOICE: 'multiple_choice', + MULTIPLE_SELECT: 'multiple_select', + PATH: 'path', + TEXT: 'text', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TRefType.ts b/packages/questionable-core/src/metadata/properties/type/TRefType.ts new file mode 100644 index 00000000..aee776b2 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TRefType.ts @@ -0,0 +1,30 @@ +import { ACTION_TYPE, TActionType } from './TActionType'; +import { ANSWER_TYPE, TAnswerType } from './TAnswerType'; +import { BRANCH_TYPE, TBranchType } from './TBranchType'; +import { BUTTON_TYPE, TButtonType } from './TButtonType'; +import { REQUIREMENT_TYPE, TRequirementType } from './TRequirementType'; +import { RESPONSE_TYPE, TResponseType } from './TResponseType'; +import { RESULT_TYPE, TResultType } from './TResultType'; +import { SECTION_TYPE, TSectionType } from './TSectionType'; +import { STEP_TYPE, TStepType } from './TStepType'; + +export type TRefType = TStepType | + TActionType | + TAnswerType | + TBranchType | + TButtonType | + TRequirementType | + TResponseType | + TResultType | + TSectionType; +export const REF_TYPE = { + ...STEP_TYPE, + ...ACTION_TYPE, + ...ANSWER_TYPE, + ...BRANCH_TYPE, + ...BUTTON_TYPE, + ...RESULT_TYPE, + ...REQUIREMENT_TYPE, + ...RESPONSE_TYPE, + ...SECTION_TYPE, +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TRequirementType.ts b/packages/questionable-core/src/metadata/properties/type/TRequirementType.ts new file mode 100644 index 00000000..a3460d55 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TRequirementType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TRequirementType = 'required' | 'non-required'; +type TEnmRequirementType = TEnmBaseType & { + NON_REQUIRED: TRequirementType & 'non-required'; + REQUIRED: TRequirementType & 'required'; +}; +export const REQUIREMENT_TYPE: TEnmRequirementType = { + ...BASE_TYPE, + NON_REQUIRED: 'non-required', + REQUIRED: 'required', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TResponseType.ts b/packages/questionable-core/src/metadata/properties/type/TResponseType.ts new file mode 100644 index 00000000..f70fb495 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TResponseType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TResponseType = 'complete' | 'incomplete'; +type TEnmResponseType = TEnmBaseType & { + COMPLETE: TResponseType & 'complete'; + INCOMPLETE: TResponseType & 'incomplete'; +}; +export const RESPONSE_TYPE: TEnmResponseType = { + ...BASE_TYPE, + COMPLETE: 'complete', + INCOMPLETE: 'incomplete', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TResultType.ts b/packages/questionable-core/src/metadata/properties/type/TResultType.ts new file mode 100644 index 00000000..3efe5876 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TResultType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TResultType = 'match' | 'non-match'; +type TEnmResultType = TEnmBaseType & { + MATCH: TResultType & 'match'; + NON_MATCH: TResultType & 'non-match'; +}; +export const RESULT_TYPE: TEnmResultType = { + ...BASE_TYPE, + MATCH: 'match', + NON_MATCH: 'non-match', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TSectionType.ts b/packages/questionable-core/src/metadata/properties/type/TSectionType.ts new file mode 100644 index 00000000..9a742d00 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TSectionType.ts @@ -0,0 +1,12 @@ +import { BASE_TYPE, TEnmBaseType } from './TBaseType'; + +export type TSectionType = 'locked' | 'unlocked'; +type TEnmSectionType = TEnmBaseType & { + LOCKED: TSectionType & 'locked'; + UNLOCKED: TSectionType & 'unlocked'; +}; +export const SECTION_TYPE: TEnmSectionType = { + ...BASE_TYPE, + LOCKED: 'locked', + UNLOCKED: 'unlocked', +}; diff --git a/packages/questionable-core/src/metadata/properties/type/TStepType.ts b/packages/questionable-core/src/metadata/properties/type/TStepType.ts new file mode 100644 index 00000000..4e05de5e --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/TStepType.ts @@ -0,0 +1,15 @@ +import { PAGE_TYPE, TPageType } from './TPageType'; +import { QUESTION_TYPE, TQuestionType } from './TQuestionType'; +import { BASE_TYPE, TBaseType } from './TBaseType'; + +// type TStepType = TEnmPageType | TEnmQuestionType | TDesignType | TEnmBase; + +export type TStepType = TPageType | TQuestionType | TBaseType; +/** + * Defines the type of step for UI rendering + */ +export const STEP_TYPE = { + ...PAGE_TYPE, + ...QUESTION_TYPE, + ...BASE_TYPE, +}; diff --git a/packages/questionable-core/src/metadata/properties/type/_exports.ts b/packages/questionable-core/src/metadata/properties/type/_exports.ts new file mode 100644 index 00000000..5cae7b43 --- /dev/null +++ b/packages/questionable-core/src/metadata/properties/type/_exports.ts @@ -0,0 +1,13 @@ +export * from './TActionType'; +export * from './TAnswerType'; +export * from './TBaseType'; +export * from './TBranchType'; +export * from './TButtonType'; +export * from './TPageType'; +export * from './TQuestionType'; +export * from './TRefType'; +export * from './TRequirementType'; +export * from './TResponseType'; +export * from './TResultType'; +export * from './TSectionType'; +export * from './TStepType'; diff --git a/packages/questionable-core/src/metadata/types/ClassProperties.ts b/packages/questionable-core/src/metadata/types/ClassProperties.ts new file mode 100644 index 00000000..a4d63fe1 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/ClassProperties.ts @@ -0,0 +1,16 @@ +/** + * Construct a type using property names + * @hidden + */ +export type ClassProperties = FlatStrings>; +// type PrivateProperties = T[keyof ClassProperties] +/** + * Flatten object + * @hidden + */ +export type FlatStrings = T extends object ? T[keyof T] : T; +/** + * Grab the properties + * @hidden + */ +export type CoreProperties = X[keyof X]; diff --git a/packages/questionable-core/src/metadata/types/TAgeCore.ts b/packages/questionable-core/src/metadata/types/TAgeCore.ts new file mode 100644 index 00000000..46447779 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TAgeCore.ts @@ -0,0 +1,33 @@ +/* + * Defines an age relative to a date + * @title Age Type + */ +export interface TAgeCore { + /** + * @minimum 0 + * @maximum 31 + * @nullable + * @title Days + */ + days?: number; + /** + * @minimum 0 + * @maximum 31 + * @nullable + * @title Months + */ + months: number; + /** + * @minimum 0 + * @maximum 100 + * @nullable + * @title Years + */ + years: number; +} + +export interface TDateOfBirthCore { + day?: string | undefined; + month?: string | undefined; + year?: string | undefined; +} diff --git a/packages/questionable-core/src/metadata/types/TAnswerDataCore.ts b/packages/questionable-core/src/metadata/types/TAnswerDataCore.ts new file mode 100644 index 00000000..1919a874 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TAnswerDataCore.ts @@ -0,0 +1,16 @@ +import { IQuestionCore } from '../IQuestionCore'; +import { IResponseCore } from '../IResponseCore'; + +/** + * Event data structure to be sent with event callbacks + * @title Event Data Type + */ + +export type TAnswerDataCore = { + answer: string; + responses: IResponseCore[] | IQuestionCore[]; + /** + * @hidden + */ + step: IQuestionCore; +}; diff --git a/packages/questionable-core/src/metadata/types/TCollectable.ts b/packages/questionable-core/src/metadata/types/TCollectable.ts new file mode 100644 index 00000000..1012ce64 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TCollectable.ts @@ -0,0 +1,8 @@ +import { TTypeable } from './TTypeable'; + +type TAdd = (param: TCollectable) => void; + +export type TCollectable = TTypeable & { + add?: TAdd; + title: string; +}; diff --git a/packages/questionable-core/src/metadata/types/TContentCore.ts b/packages/questionable-core/src/metadata/types/TContentCore.ts new file mode 100644 index 00000000..b67adb8c --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TContentCore.ts @@ -0,0 +1,20 @@ +/** + * Content type for blocks of copy + */ +export interface TContentCore { + /** + * Main body content + * @title Content + */ + content?: string; + /** + * Text to display below the title + * @title Subtitle + */ + subTitle?: string; + /** + * Title or Header text + * @title Title + */ + title?: string; +} diff --git a/packages/questionable-core/src/metadata/types/TDesignType.ts b/packages/questionable-core/src/metadata/types/TDesignType.ts new file mode 100644 index 00000000..a223e22b --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TDesignType.ts @@ -0,0 +1,10 @@ +/** + * Defines the known component types for design + */ +export type TDesignType = 'Edit'; +type TEnmDesignType = { + EDIT: TDesignType & 'Edit'; +}; +export const DESIGN_TYPE: TEnmDesignType = { + EDIT: 'Edit', +}; diff --git a/packages/questionable-core/src/metadata/types/TGateCore.ts b/packages/questionable-core/src/metadata/types/TGateCore.ts new file mode 100644 index 00000000..0c1811f6 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TGateCore.ts @@ -0,0 +1,57 @@ +import { TStringDictionaryCore } from './TStringDictionaryCore'; +import { IFormCore } from '../IFormCore'; +import { TAnswerDataCore } from './TAnswerDataCore'; +import { TPageDataCore } from './TPageDataCore'; +import { TResultDataCore } from './TResultDataCore'; + +/** + * Represents any type of mutation which has significant impact + * + * @title Gate Type + */ + +export type TGateCore = 'branch' | 'age'; + +export type TGateDataCore = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + gate: TGateCore; +}; +/** + * Generic data input for event context + */ + +export type TEventCore = TPageDataCore | + TAnswerDataCore | + TResultDataCore | + IFormCore | + TGateDataCore; +/** + * Event function type to be used as a callback + * + * @title Event Type + * @hidden + */ + +export type TOnEventCore = (data: TEventCore) => void; +/** + * Error function type to be used as a callback + * + * @title Error Type + * @hidden + */ + +export type TOnErrorCore = (e: Error, data?: TEventCore) => void; +/** + * Event triggered when gates are switched + * + * @title Gate Switch Event + * + * @title Gate Switch Event + * @hidden + */ + +export type TOnGateSwitchCore = ( + gate: TGateCore, + data: TStringDictionaryCore +) => void; diff --git a/packages/questionable-core/src/metadata/types/THorizontalPositionCore.ts b/packages/questionable-core/src/metadata/types/THorizontalPositionCore.ts new file mode 100644 index 00000000..4583039c --- /dev/null +++ b/packages/questionable-core/src/metadata/types/THorizontalPositionCore.ts @@ -0,0 +1,8 @@ +export type THorizontalPositionCore = 'left' | 'right'; +export const HORIZONTAL_POSITION: { + LEFT: THorizontalPositionCore & 'left'; + RIGHT: THorizontalPositionCore & 'right'; +} = { + LEFT: 'left', + RIGHT: 'right', +}; diff --git a/packages/questionable-core/src/metadata/types/TOpType.ts b/packages/questionable-core/src/metadata/types/TOpType.ts new file mode 100644 index 00000000..bc93daf0 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TOpType.ts @@ -0,0 +1,13 @@ +export type TOpType = 'RERENDER' | 'RESET' | 'undo' | 'UPDATE'; +type TEnmOpType = { + RERENDER: TOpType & 'RERENDER'; + RESET: TOpType & 'RESET'; + UNDO: TOpType & 'undo'; + UPDATE: TOpType & 'UPDATE'; +}; +export const OP_TYPE: TEnmOpType = { + RERENDER: 'RERENDER', + RESET: 'RESET', + UNDO: 'undo', + UPDATE: 'UPDATE', +}; diff --git a/packages/questionable-core/src/metadata/types/TPageDataCore.ts b/packages/questionable-core/src/metadata/types/TPageDataCore.ts new file mode 100644 index 00000000..cff16728 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TPageDataCore.ts @@ -0,0 +1,14 @@ +import { DIRECTION } from '../../lib/enums'; + +/** + * Event data structure to be sent with event callbacks + * @title Event Data Type + */ + +export type TPageDataCore = { + dir: DIRECTION; + /** + * @hidden + */ + step: unknown; +}; diff --git a/packages/questionable-core/src/metadata/types/TProgressBarStatusType.ts b/packages/questionable-core/src/metadata/types/TProgressBarStatusType.ts new file mode 100644 index 00000000..f271a00b --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TProgressBarStatusType.ts @@ -0,0 +1,14 @@ +export type TProgressBarStatusType = 'complete' | 'current' | 'incomplete'; +type TEnmProgressBarStatusType = { + COMPLETE: TProgressBarStatusType & 'complete'; + CURRENT: TProgressBarStatusType & 'current'; + INCOMPLETE: TProgressBarStatusType & 'incomplete'; +}; +/** + * Progress Bar status + */ +export const PROGRESS_BAR_STATUS: TEnmProgressBarStatusType = { + COMPLETE: 'complete', + CURRENT: 'current', + INCOMPLETE: 'incomplete', +}; diff --git a/packages/questionable-core/src/metadata/types/TReferentialble.ts b/packages/questionable-core/src/metadata/types/TReferentialble.ts new file mode 100644 index 00000000..02e94b4a --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TReferentialble.ts @@ -0,0 +1,27 @@ +import { IRefCore } from '../IRefCore'; +import { TCollectable } from './TCollectable'; + +// const tryParseQuestion = (value: TTypeable): Q | undefined => { +// if (isQuestion(value)) { +// return value as Q; +// } +// return undefined; +// }; +// const tryParsePage = (value: TTypeable): Q | undefined => { +// if (isPage(value)) { +// return value as Q; +// } +// return undefined; +// }; +export type TReferentialble = IRefCore & { + actions?: TCollectable[]; + answers?: TCollectable[]; + branches?: TCollectable[]; + buttons?: TCollectable[]; + // [key in TPoolName]: TCollectable[]; + questions?: TCollectable[]; + requirements?: TCollectable[]; + responses?: TCollectable[]; + results?: TCollectable[]; + sections?: TCollectable[]; +}; diff --git a/packages/questionable-core/src/metadata/types/TResultDataCore.ts b/packages/questionable-core/src/metadata/types/TResultDataCore.ts new file mode 100644 index 00000000..e468e76d --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TResultDataCore.ts @@ -0,0 +1,9 @@ +/** + * Event data structure for results + * @title Event Result Type + */ + +export type TResultDataCore = { + results: unknown[]; + step: unknown; +}; diff --git a/packages/questionable-core/src/metadata/types/TStringDictionaryCore.ts b/packages/questionable-core/src/metadata/types/TStringDictionaryCore.ts new file mode 100644 index 00000000..b9c14af6 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TStringDictionaryCore.ts @@ -0,0 +1,12 @@ +/** + * Key/value pairs which are both strings + */ +export type TStringDictionaryCore = { + [key: string]: string; +}; +/** + * Generic fetch dictionary + * + * @hidden Functions must be hidden from schema + */ +export type TGetDictionaryCore = (...params: unknown[]) => TStringDictionaryCore; diff --git a/packages/questionable-core/src/metadata/types/TTypeable.ts b/packages/questionable-core/src/metadata/types/TTypeable.ts new file mode 100644 index 00000000..e39fc388 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TTypeable.ts @@ -0,0 +1,7 @@ +import { TInstanceOf } from '../../lib/instanceOf'; +import { TRefType } from '../properties/type/TRefType'; + +export type TTypeable = { + instanceOfCheck: TInstanceOf; + type: TRefType; +}; diff --git a/packages/questionable-core/src/metadata/types/TVerticalPositionCore.ts b/packages/questionable-core/src/metadata/types/TVerticalPositionCore.ts new file mode 100644 index 00000000..e09ea6cb --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TVerticalPositionCore.ts @@ -0,0 +1,8 @@ +export type TVerticalPositionCore = 'top' | 'bottom'; +export const VERTICAL_POSITION: { + BOTTOM: TVerticalPositionCore & 'bottom'; + TOP: TVerticalPositionCore & 'top'; +} = { + BOTTOM: 'bottom', + TOP: 'top', +}; diff --git a/packages/questionable-core/src/metadata/types/TVisible.ts b/packages/questionable-core/src/metadata/types/TVisible.ts new file mode 100644 index 00000000..1a3e7988 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/TVisible.ts @@ -0,0 +1,3 @@ +export type TVisible = { + visible?: boolean; +}; diff --git a/packages/questionable-core/src/metadata/types/_exports.ts b/packages/questionable-core/src/metadata/types/_exports.ts new file mode 100644 index 00000000..de7b0b65 --- /dev/null +++ b/packages/questionable-core/src/metadata/types/_exports.ts @@ -0,0 +1,14 @@ +export * from './ClassProperties'; +export * from './TAgeCore'; +export * from './TAnswerDataCore'; +export * from './TContentCore'; +export * from './TDesignType'; +export * from './TGateCore'; +export * from './THorizontalPositionCore'; +export * from './TOpType'; +export * from './TPageDataCore'; +export * from './TProgressBarStatusType'; +export * from './TResultDataCore'; +export * from './TStringDictionaryCore'; +export * from './TVerticalPositionCore'; +export * from './TVisible'; diff --git a/packages/questionable-core/src/schema/index.ts b/packages/questionable-core/src/schema/_exports.ts similarity index 100% rename from packages/questionable-core/src/schema/index.ts rename to packages/questionable-core/src/schema/_exports.ts diff --git a/packages/questionable-core/src/schema/editStepSchema.ts b/packages/questionable-core/src/schema/editStepSchema.ts index 19abd51a..58d99986 100644 --- a/packages/questionable-core/src/schema/editStepSchema.ts +++ b/packages/questionable-core/src/schema/editStepSchema.ts @@ -1,8 +1,10 @@ -import { merge } from 'lodash'; -import { isEnum, PAGE_TYPE, QUESTION_TYPE } from '../util/enums'; -import { IPageDataCore } from '../survey/IPageDataCore'; -import { IQuestionDataCore } from '../survey/IQuestionDataCore'; -import { survey } from './survey'; +import { merge } from 'lodash'; +import { IPageCore } from '../metadata/IPageCore'; +import { PAGE_TYPE } from '../metadata/properties/type/TPageType'; +import { IQuestionCore } from '../metadata/IQuestionCore'; +import { QUESTION_TYPE } from '../metadata/properties/type/TQuestionType'; +import { isEnum } from '../lib/enums'; +import { survey } from './survey'; const schemaPart = { properties: { @@ -22,12 +24,12 @@ const schemaFull: any = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getStepSchema = (props: IQuestionDataCore | IPageDataCore): any => { +export function getStepSchema({ step }: { step: IQuestionCore | IPageCore; }): any { const schemaProps = { ...schemaPart }; - if (isEnum(PAGE_TYPE, props.step.type)) { + if (isEnum({ enm: PAGE_TYPE, value: step.type })) { schemaProps.properties.step.$ref = '#/definitions/IPageCore'; - } else if (isEnum(QUESTION_TYPE, props.step.type)) { + } else if (isEnum({ enm: QUESTION_TYPE, value: step.type })) { schemaProps.properties.step.$ref = '#/definitions/IQuestionCore'; } return merge(schemaProps, schemaFull); -}; +} diff --git a/packages/questionable-core/src/schema/survey.ts b/packages/questionable-core/src/schema/survey.ts index 47c9bc90..0874889f 100644 --- a/packages/questionable-core/src/schema/survey.ts +++ b/packages/questionable-core/src/schema/survey.ts @@ -3,11 +3,6 @@ export const survey = { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "DESIGN_TYPE": { - "const": "Edit", - "description": "Defines the known component types for design", - "type": "string" - }, "DIRECTION": { "description": "Navigation direction for steps by array index (+1 or -1)", "enum": [ @@ -38,72 +33,68 @@ export const survey = { } }, "required": [ - "id", - "label" + "title" ], "type": "object" }, - "IBranchCore": { + "IAnswerCore": { "properties": { "id": { "description": "Unique identifier", "title": "Id", "type": "string" }, - "questions": { + "key": { + "type": "string" + }, + "synonyms": { "items": { - "$ref": "#/definitions/IRefCore" + "type": "string" }, "type": "array" }, "title": { "title": "Title", "type": "string" + }, + "type": { + "$ref": "#/definitions/TAnswerType" } }, "required": [ - "id", - "questions" + "title" ], "type": "object" }, - "IButtonConfigCore": { - "description": "Configuration for buttons", + "IBranchCore": { "properties": { - "core": { - "enum": [ - "IButtonConfig", - "I" - ], - "type": "string" - }, "id": { "description": "Unique identifier", "title": "Id", "type": "string" }, - "link": { - "description": "Link to tie to button click", - "title": "Link", - "type": "string" + "questions": { + "items": { + "$ref": "#/definitions/IQuestionCore" + }, + "type": "array" + }, + "sections": { + "items": { + "$ref": "#/definitions/ISectionCore" + }, + "type": "array" }, "title": { "title": "Title", "type": "string" }, "type": { - "$ref": "#/definitions/TButtonModeCore", - "description": "Render mode (link or button)", - "title": "Mode" - }, - "visible": { - "description": "Visibility status of the button (show/hide)", - "title": "Visible", - "type": "boolean" + "$ref": "#/definitions/TBranchType" } }, "required": [ - "id" + "title" ], "type": "object" }, @@ -125,7 +116,7 @@ export const survey = { "type": "string" }, "type": { - "$ref": "#/definitions/TButtonModeCore", + "$ref": "#/definitions/TButtonType", "description": "Render mode (link or button)", "title": "Mode" }, @@ -136,69 +127,12 @@ export const survey = { } }, "required": [ - "id" - ], - "type": "object" - }, - "IDesignDataCore": { - "description": "Data defintion for design step", - "properties": { - "form": { - "$ref": "#/definitions/IFormCore", - "description": "The user's current form state", - "title": "FormCore" - }, - "step": { - "$ref": "#/definitions/IStepCore", - "description": "Current step" - }, - "stepId": { - "description": "Internally unique identifier", - "title": "Step ID", - "type": [ - "string", - "number" - ] - } - }, - "required": [ - "form", - "step", - "stepId" + "title" ], "type": "object" }, "IEventCore": { "description": "Event Model", - "properties": { - "onActionClick": { - "not": {} - }, - "onAnswer": { - "not": {} - }, - "onAnyEvent": { - "not": {} - }, - "onError": { - "not": {} - }, - "onGateSwitch": { - "not": {} - }, - "onInit": { - "not": {} - }, - "onNoResults": { - "not": {} - }, - "onPage": { - "not": {} - }, - "onResults": { - "not": {} - } - }, "title": "Event", "type": "object" }, @@ -206,9 +140,42 @@ export const survey = { "description": "Represents the survey as completed by the user", "properties": { "age": { - "$ref": "#/definitions/TAgeCore", "description": "Customer's age in years/months/days", - "title": "Age" + "properties": { + "days": { + "maximum": 31, + "minimum": 0, + "title": "Days", + "type": [ + "number", + "null" + ] + }, + "months": { + "maximum": 31, + "minimum": 0, + "title": "Months", + "type": [ + "number", + "null" + ] + }, + "years": { + "maximum": 100, + "minimum": 0, + "title": "Years", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "months", + "years" + ], + "title": "Age", + "type": "object" }, "birthdate": { "description": "Customer's entered birthdate", @@ -237,39 +204,10 @@ export const survey = { } }, "required": [ - "responses", "started" ], "type": "object" }, - "INavigationConfigCore": { - "description": "Configuration for navigation", - "properties": { - "core": { - "enum": [ - "INavigationConfig", - "I" - ], - "type": "string" - } - }, - "type": "object" - }, - "IPageConfigCore": { - "properties": { - "core": { - "enum": [ - "IPageConfig", - "I" - ], - "type": "string" - }, - "visible": { - "type": "boolean" - } - }, - "type": "object" - }, "IPageCore": { "description": "Defines step content for Page types", "properties": { @@ -288,6 +226,9 @@ export const survey = { "title": "Body Subheading", "type": "string" }, + "display": { + "type": "boolean" + }, "entryRequirements": { "description": "Collection of requirements to view/enter this step", "items": { @@ -325,21 +266,9 @@ export const survey = { "type": "string" }, "section": { + "$ref": "#/definitions/ISectionCore", "description": "Section to which this step belongs", - "properties": { - "id": {}, - "requirements": { - "description": "Collection of requirements to enable display of this status", - "items": { - "$ref": "#/definitions/IRequirementCore" - }, - "title": "Requirements", - "type": "array" - }, - "title": {} - }, - "title": "Section", - "type": "object" + "title": "Section" }, "subTitle": { "description": "Text to display below the title", @@ -351,58 +280,18 @@ export const survey = { "type": "string" }, "type": { - "$ref": "#/definitions/PAGE_TYPE", + "$ref": "#/definitions/TPageType", "description": "Type of page", "title": "Page Type" } }, "required": [ - "id", - "section", + "display", + "title", "type" ], "type": "object" }, - "IPageDataCore": { - "description": "Data defintion for page step", - "properties": { - "form": { - "$ref": "#/definitions/IFormCore", - "description": "The user's current form state", - "title": "FormCore" - }, - "step": { - "$ref": "#/definitions/IPageCore", - "description": "Current step" - }, - "stepId": { - "description": "Internally unique identifier", - "title": "Step ID", - "type": [ - "string", - "number" - ] - } - }, - "required": [ - "form", - "step", - "stepId" - ], - "type": "object" - }, - "IPagesConfigCore": { - "properties": { - "core": { - "enum": [ - "IPagesConfig", - "I" - ], - "type": "string" - } - }, - "type": "object" - }, "IPagesCore": { "description": "Defines required pages for the survey flow", "properties": { @@ -416,6 +305,12 @@ export const survey = { "description": "Last step of the survey if there are 0 results", "title": "No Results Page" }, + "pages": { + "items": { + "$ref": "#/definitions/IPageCore" + }, + "type": "array" + }, "resultsPage": { "$ref": "#/definitions/IPageCore", "description": "Last step of the survey if there are 1 or more results", @@ -427,46 +322,14 @@ export const survey = { "title": "Summary Page" } }, - "required": [ - "noResultsPage", - "resultsPage", - "summaryPage" - ], - "type": "object" - }, - "IProgressBarConfigCore": { - "description": "Configuration options for the progress bar", - "properties": { - "core": { - "enum": [ - "IProgressBarConfig", - "I" - ], - "type": "string" - } - }, - "type": "object" - }, - "IQuestionConfigCore": { - "description": "Configuration for question display", - "properties": { - "core": { - "enum": [ - "IQuestionConfig", - "I" - ], - "type": "string" - } - }, "type": "object" }, "IQuestionCore": { - "description": "Defines step content for Question type", "properties": { "answers": { "description": "Collection of allowed answers", "items": { - "$ref": "#/definitions/IRefCore" + "$ref": "#/definitions/IAnswerCore" }, "title": "Answers", "type": "array" @@ -508,21 +371,9 @@ export const survey = { "type": "string" }, "section": { + "$ref": "#/definitions/ISectionCore", "description": "Section to which this step belongs", - "properties": { - "id": {}, - "requirements": { - "description": "Collection of requirements to enable display of this status", - "items": { - "$ref": "#/definitions/IRequirementCore" - }, - "title": "Requirements", - "type": "array" - }, - "title": {} - }, - "title": "Section", - "type": "object" + "title": "Section" }, "subTitle": { "description": "Text to display below the title", @@ -534,54 +385,18 @@ export const survey = { "type": "string" }, "type": { - "$ref": "#/definitions/QUESTION_TYPE", + "$ref": "#/definitions/TQuestionType", "description": "Type of question", "title": "Question Type" } }, "required": [ "answers", - "id", - "section", + "title", "type" ], "type": "object" }, - "IQuestionDataCore": { - "description": "Data defintion for question step", - "properties": { - "core": { - "enum": [ - "IQuestionData", - "I" - ], - "type": "string" - }, - "form": { - "$ref": "#/definitions/IFormCore", - "description": "The user's current form state", - "title": "FormCore" - }, - "step": { - "$ref": "#/definitions/IQuestionCore", - "description": "Current step" - }, - "stepId": { - "description": "Internally unique identifier", - "title": "Step ID", - "type": [ - "string", - "number" - ] - } - }, - "required": [ - "form", - "step", - "stepId" - ], - "type": "object" - }, "IQuestionableConfigCore": { "description": "Configuration for customized behavior of Questionable", "properties": { @@ -594,12 +409,8 @@ export const survey = { "nav": { "description": "Navigation configuration", "properties": { - "core": { - "enum": [ - "INavigationConfig", - "I" - ], - "type": "string" + "visible": { + "type": "boolean" } }, "title": "Navigation", @@ -608,26 +419,27 @@ export const survey = { "pages": { "description": "Page configuration", "properties": { - "core": { - "enum": [ - "IPagesConfig", - "I" - ], - "type": "string" + "visible": { + "type": "boolean" } }, "title": "Pages", "type": "object" }, + "params": { + "additionalProperties": { + "type": "string" + }, + "default": {}, + "description": "Properties produced from `getRuntimeConfig()`", + "title": "Params", + "type": "object" + }, "progressBar": { "description": "Progress Bar configuration", "properties": { - "core": { - "enum": [ - "IProgressBarConfig", - "I" - ], - "type": "string" + "visible": { + "type": "boolean" } }, "title": "Progress Bar", @@ -636,12 +448,8 @@ export const survey = { "questions": { "description": "Question configuration", "properties": { - "core": { - "enum": [ - "IQuestionConfig", - "I" - ], - "type": "string" + "visible": { + "type": "boolean" } }, "title": "Question Configuration", @@ -650,23 +458,14 @@ export const survey = { "steps": { "description": "Step configuration", "properties": { - "core": { - "enum": [ - "IStepConfig", - "I" - ], - "type": "string" + "visible": { + "type": "boolean" } }, "title": "Step Configuration", "type": "object" } }, - "required": [ - "mode", - "nav", - "pages" - ], "type": "object" }, "IQuestionnaireCore": { @@ -713,14 +512,8 @@ export const survey = { } }, "required": [ - "actions", - "branches", - "config", - "header", "pages", - "questions", - "results", - "sections" + "questions" ], "type": "object" }, @@ -738,7 +531,7 @@ export const survey = { } }, "required": [ - "id" + "title" ], "type": "object" }, @@ -750,15 +543,86 @@ export const survey = { "title": "Exlanation", "type": "string" }, + "id": { + "description": "Unique identifier", + "title": "Id", + "type": "string" + }, "maxAge": { - "$ref": "#/definitions/TAgeCore", "description": "Optional maximum age allowed for this requirement", - "title": "Maximum Age" + "properties": { + "days": { + "maximum": 31, + "minimum": 0, + "title": "Days", + "type": [ + "number", + "null" + ] + }, + "months": { + "maximum": 31, + "minimum": 0, + "title": "Months", + "type": [ + "number", + "null" + ] + }, + "years": { + "maximum": 100, + "minimum": 0, + "title": "Years", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "months", + "years" + ], + "title": "Maximum Age", + "type": "object" }, "minAge": { - "$ref": "#/definitions/TAgeCore", "description": "Optional minimum age allowed for this requirement", - "title": "Minimum Age" + "properties": { + "days": { + "maximum": 31, + "minimum": 0, + "title": "Days", + "type": [ + "number", + "null" + ] + }, + "months": { + "maximum": 31, + "minimum": 0, + "title": "Months", + "type": [ + "number", + "null" + ] + }, + "years": { + "maximum": 100, + "minimum": 0, + "title": "Years", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "months", + "years" + ], + "title": "Minimum Age", + "type": "object" }, "responses": { "description": "Map of step id to required answer values", @@ -767,10 +631,19 @@ export const survey = { }, "title": "Answers", "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "$ref": "#/definitions/TRequirementType" } }, "required": [ - "responses" + "explanation", + "responses", + "title" ], "type": "object" }, @@ -779,52 +652,29 @@ export const survey = { "properties": { "answers": { "items": { - "properties": { - "id": { - "description": "Unique identifier", - "title": "Id", - "type": "string" - }, - "title": { - "title": "Title", - "type": "string" - } - }, - "type": "object" + "$ref": "#/definitions/IAnswerCore" }, "type": "array" }, + "id": { + "description": "Unique identifier", + "title": "Id", + "type": "string" + }, "question": { - "properties": { - "answers": { - "description": "Collection of allowed answers", - "items": { - "$ref": "#/definitions/IRefCore" - }, - "title": "Answers", - "type": "array" - }, - "entryRequirements": {}, - "exitRequirements": {}, - "footer": {}, - "id": {}, - "info": {}, - "internalNotes": {}, - "section": {}, - "subTitle": {}, - "title": {}, - "type": { - "$ref": "#/definitions/QUESTION_TYPE", - "description": "Type of question", - "title": "Question Type" - } - }, - "type": "object" + "$ref": "#/definitions/IQuestionCore" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "$ref": "#/definitions/TResponseType" } }, "required": [ "answers", - "question" + "title" ], "type": "object" }, @@ -861,12 +711,13 @@ export const survey = { "title": { "title": "Title", "type": "string" + }, + "type": { + "$ref": "#/definitions/TResultType" } }, "required": [ - "id", - "label", - "requirements" + "title" ], "type": "object" }, @@ -889,27 +740,16 @@ export const survey = { "title": { "title": "Title", "type": "string" + }, + "type": { + "$ref": "#/definitions/TSectionType" } }, "required": [ - "id", - "requirements" + "title" ], "type": "object" }, - "IStepConfigCore": { - "description": "Customizations for styling and formatting of the steps", - "properties": { - "core": { - "enum": [ - "IStepConfig", - "I" - ], - "type": "string" - } - }, - "type": "object" - }, "IStepCore": { "description": "Generic step data definition. Applies to all types of steps.", "properties": { @@ -950,21 +790,9 @@ export const survey = { "type": "string" }, "section": { + "$ref": "#/definitions/ISectionCore", "description": "Section to which this step belongs", - "properties": { - "id": {}, - "requirements": { - "description": "Collection of requirements to enable display of this status", - "items": { - "$ref": "#/definitions/IRequirementCore" - }, - "title": "Requirements", - "type": "array" - }, - "title": {} - }, - "title": "Section", - "type": "object" + "title": "Section" }, "subTitle": { "description": "Text to display below the title", @@ -982,123 +810,100 @@ export const survey = { } }, "required": [ - "id", - "section", + "title", "type" ], "type": "object" }, - "IStepDataCore": { - "description": "Data defintion for base wizard step", + "MODE": { + "enum": [ + "dev", + "edit", + "view" + ], + "type": "string" + }, + "TActionType": { + "enum": [ + "call", + "hybrid", + "none", + "online", + "shell" + ], + "type": "string" + }, + "TAnswerDataCore": { + "description": "Event data structure to be sent with event callbacks", "properties": { - "form": { - "$ref": "#/definitions/IFormCore", - "description": "The user's current form state", - "title": "FormCore" + "answer": { + "type": "string" }, - "step": { - "$ref": "#/definitions/IStepCore", - "description": "Current step", - "title": "Step" - }, - "stepId": { - "description": "Internally unique identifier", - "title": "Step ID", - "type": [ - "string", - "number" + "responses": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/IResponseCore" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/definitions/IQuestionCore" + }, + "type": "array" + } ] } }, "required": [ - "form", - "stepId" + "answer", + "responses" ], + "title": "Event Data Type", "type": "object" }, - "MODE": { + "TAnswerType": { "enum": [ - "dev", - "edit", - "view" + "fixed", + "variable" ], "type": "string" }, - "PAGE_TYPE": { - "description": "Defines the known component types for pages", + "TBaseType": { + "const": "default", + "type": "string" + }, + "TBranchType": { "enum": [ - "Landing", - "No Results", - "Results", - "Summary" + "linear", + "non-linear" ], "type": "string" }, - "QUESTION_TYPE": { - "description": "Defines the known component types for questions", + "TButtonType": { "enum": [ - "dob", - "multiple_choice", - "multiple_select" + "button", + "link" ], "type": "string" }, - "TAgeCore": { - "properties": { - "days": { - "maximum": 31, - "minimum": 0, - "title": "Days", - "type": [ - "number", - "null" - ] - }, - "months": { - "maximum": 31, - "minimum": 0, - "title": "Months", - "type": [ - "number", - "null" - ] - }, - "years": { - "maximum": 100, - "minimum": 0, - "title": "Years", - "type": [ - "number", - "null" - ] - } - }, - "required": [ - "months", - "years" - ], - "type": "object" + "TDesignType": { + "const": "Edit", + "description": "Defines the known component types for design", + "type": "string" }, - "TAnswerDataCore": { - "description": "Event data structure to be sent with event callbacks", + "TEnmBaseType": { "properties": { - "answer": { - "type": "string" - }, - "step": { - "type": "string" + "DEFAULT": { + "$ref": "#/definitions/TBaseType" } }, "required": [ - "answer", - "step" + "DEFAULT" ], - "title": "Event Data Type", "type": "object" }, - "TButtonModeCore": { - "type": "string" - }, "TEventCore": { "anyOf": [ { @@ -1141,77 +946,156 @@ export const survey = { ], "type": "object" }, + "TOpType": { + "enum": [ + "RERENDER", + "RESET", + "undo", + "UPDATE" + ], + "type": "string" + }, "TPageDataCore": { "description": "Event data structure to be sent with event callbacks", "properties": { "dir": { "$ref": "#/definitions/DIRECTION" - }, - "step": { - "type": "string" } }, "required": [ - "dir", - "step" + "dir" ], "title": "Event Data Type", "type": "object" }, + "TPageType": { + "description": "Defines the known component types for pages", + "enum": [ + "Landing", + "No Results", + "Results", + "Summary" + ], + "type": "string" + }, + "TPages": { + "additionalProperties": { + "$ref": "#/definitions/IPageCore" + }, + "type": "object" + }, + "TProgressBarStatusType": { + "enum": [ + "complete", + "current", + "incomplete" + ], + "type": "string" + }, + "TQuestionType": { + "description": "Defines the known component types for questions", + "enum": [ + "date_time", + "dob", + "multiple_choice", + "multiple_select", + "path", + "text" + ], + "type": "string" + }, + "TRefType": { + "anyOf": [ + { + "$ref": "#/definitions/TStepType" + }, + { + "$ref": "#/definitions/TActionType" + }, + { + "$ref": "#/definitions/TAnswerType" + }, + { + "$ref": "#/definitions/TBranchType" + }, + { + "$ref": "#/definitions/TButtonType" + }, + { + "$ref": "#/definitions/TRequirementType" + }, + { + "$ref": "#/definitions/TResponseType" + }, + { + "$ref": "#/definitions/TResultType" + }, + { + "$ref": "#/definitions/TSectionType" + } + ] + }, + "TRequirementType": { + "enum": [ + "required", + "non-required" + ], + "type": "string" + }, + "TResponseType": { + "enum": [ + "complete", + "incomplete" + ], + "type": "string" + }, "TResultDataCore": { "description": "Event data structure for results", "properties": { - "props": { - "$ref": "#/definitions/IStepDataCore" - }, "results": { "items": { - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": [ - "id", - "label", - "reason" - ], - "type": "object" + "$ref": "#/definitions/IResultCore" }, "type": "array" }, "step": { - "const": "results", - "type": "string" + "$ref": "#/definitions/IStepCore" } }, "required": [ - "props", "results", "step" ], "title": "Event Result Type", "type": "object" }, + "TResultType": { + "enum": [ + "match", + "non-match" + ], + "type": "string" + }, + "TSectionType": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, "TStepType": { "anyOf": [ { - "$ref": "#/definitions/PAGE_TYPE" + "$ref": "#/definitions/TPageType" + }, + { + "$ref": "#/definitions/TQuestionType" }, { - "$ref": "#/definitions/QUESTION_TYPE" + "$ref": "#/definitions/TDesignType" }, { - "$ref": "#/definitions/DESIGN_TYPE" + "$ref": "#/definitions/TBaseType" } ] } diff --git a/packages/questionable-core/src/state/_exports.ts b/packages/questionable-core/src/state/_exports.ts new file mode 100644 index 00000000..04ae0dfd --- /dev/null +++ b/packages/questionable-core/src/state/_exports.ts @@ -0,0 +1 @@ +export * from './pubsub'; diff --git a/packages/questionable-core/src/state/index.ts b/packages/questionable-core/src/state/index.ts deleted file mode 100644 index 08a834de..00000000 --- a/packages/questionable-core/src/state/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './stepReducer'; diff --git a/packages/questionable-core/src/state/pubsub.ts b/packages/questionable-core/src/state/pubsub.ts new file mode 100644 index 00000000..227ba545 --- /dev/null +++ b/packages/questionable-core/src/state/pubsub.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// This is a stubbed class, it does nothing at the moment +// Do not change the API when implementing + +type TEventType = + | 'start' + | 'finish' + | 'error' + | 'page' + | 'action' + | 'warn' + | 'answer' + | string; + +type TEvent = { + event: any; + type: TEventType; +}; + +type TSubscriber = { + trigger: any; + type: TEventType; +}; + +export class PubSub { + #events: TEvent[] = []; + + #subscribers: TSubscriber[] = []; + + async #notify({ type, event }: TEvent) { + return Promise.all( + this.#subscribers + .filter((s) => s.type === type) + .map((s) => s.trigger(event)), + ); + } + + constructor( + { + events, + subscribers, + }: { events: TEvent[]; subscribers: TSubscriber[] } = { + events: [], + subscribers: [], + }, + ) { + this.#events = events || []; + this.#subscribers = subscribers || []; + this.events = new Set( + this.#events + .map((e) => e.type) + .concat(this.#subscribers.map((s) => s.type)), + ); + } + + events = new Set(); + + publish({ type, event }: TEvent) { + this.events.add(type); + // this.#events.push({ event, type }); + return this.#notify({ event, type }); + } + + subscribe({ type, trigger }: TSubscriber) { + this.#subscribers.push({ trigger, type }); + } +} + +export const eventedCore = new PubSub(); diff --git a/packages/questionable-core/src/state/stepReducer.ts b/packages/questionable-core/src/state/stepReducer.ts deleted file mode 100644 index 5f1fcf12..00000000 --- a/packages/questionable-core/src/state/stepReducer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { merge } from 'lodash'; -import { ACTION_TYPE } from '../util/enums'; -import { FormCore } from '../composable/FormCore'; -import { IFormCore } from '../survey/IFormCore'; - -/** - * Merges the form's answer state as the user progresses through the survey - * @param previousState - * @param action - * @returns - */ -export const stepReducer = ( - previousState: IFormCore, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - action: { type: ACTION_TYPE; value: any }, -): IFormCore => { - // Action should never be null, - // except when we attempt to storybook/test individual components in isolation - switch (action?.type) { - case ACTION_TYPE.RESET: - return new FormCore(); - - case ACTION_TYPE.UPDATE: - return merge( - { - ...previousState, - }, - { - ...action.value, - }, - ); - - // Effectively a noop that triggers a re-render of the page - case ACTION_TYPE.RERENDER: - return ({ - ...previousState, - }); - - default: - return previousState; - } -}; diff --git a/packages/questionable-core/src/survey/IBranchCore.ts b/packages/questionable-core/src/survey/IBranchCore.ts deleted file mode 100644 index ab35ee4d..00000000 --- a/packages/questionable-core/src/survey/IBranchCore.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IRefCore } from './IRefCore'; - -export interface IBranchCore extends IRefCore { - questions: IRefCore[]; -} diff --git a/packages/questionable-core/src/survey/IDesignDataCore.ts b/packages/questionable-core/src/survey/IDesignDataCore.ts deleted file mode 100644 index 77ceced9..00000000 --- a/packages/questionable-core/src/survey/IDesignDataCore.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IStepCore } from './IStepCore'; -import { IStepDataCore } from './IStepDataCore'; - -/** - * Data defintion for design step - */ - -export interface IDesignDataCore extends IStepDataCore { - step: IStepCore; -} diff --git a/packages/questionable-core/src/survey/IEventCore.ts b/packages/questionable-core/src/survey/IEventCore.ts deleted file mode 100644 index 0e6fad64..00000000 --- a/packages/questionable-core/src/survey/IEventCore.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { DIRECTION } from '../util/enums'; -import { TStringDictionaryCore } from '../util/types'; -import { IFormCore } from './IFormCore'; -import { IQuestionDataCore } from './IQuestionDataCore'; -import { IStepDataCore } from './IStepDataCore'; - -/** - * Event data structure to be sent with event callbacks - * @title Event Data Type - */ -export type TPageDataCore = { - dir: DIRECTION; - /** - * @hidden - */ - props: IStepDataCore, - step: string; -} -/** - * Event data structure to be sent with event callbacks - * @title Event Data Type - */ -export type TAnswerDataCore = { - answer: string; - /** - * @hidden - */ - props: IQuestionDataCore, - step: string; -} - -/** - * Event data structure for results - * @title Event Result Type - */ -export type TResultDataCore = { - props: IStepDataCore; - results: { - id: string; - label: string; - reason: string; - title: string | undefined; - }[]; - step: 'results'; -} - -/** - * Represents any type of mutation which has significant impact - * - * @title Gate Type - */ -export type TGateCore = 'branch' | 'age'; - -export type TGateDataCore = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any, - gate: TGateCore -}; - -/** - * Generic data input for event context - */ -export type TEventCore = TPageDataCore - | TAnswerDataCore | TResultDataCore | IFormCore | TGateDataCore; - -/** - * Event function type to be used as a callback - * - * @title Event Type - * @hidden - */ -export type TOnEventCore = (data: TEventCore) => void; - -/** - * Error function type to be used as a callback - * - * @title Error Type - * @hidden - */ -export type TOnErrorCore = (e: Error, data?: TEventCore) => void; - -/** - * Event triggered when gates are switched - * - * @title Gate Switch Event - * - * @title Gate Switch Event - * @hidden - */ -export type TOnGateSwitchCore = (gate: TGateCore, data: TStringDictionaryCore) => void; - -/** - * Event Model - * @title Event - */ -export interface IEventCore { - onActionClick: TOnEventCore | undefined, - onAnswer: TOnEventCore | undefined, - onAnyEvent: TOnEventCore | undefined, - onError: TOnErrorCore | undefined, - onGateSwitch: TOnEventCore | undefined, - onInit: TOnEventCore | undefined, - onNoResults: TOnEventCore | undefined, - onPage: TOnEventCore | undefined, - onResults: TOnEventCore | undefined -} diff --git a/packages/questionable-core/src/survey/IPageDataCore.ts b/packages/questionable-core/src/survey/IPageDataCore.ts deleted file mode 100644 index 31f5d296..00000000 --- a/packages/questionable-core/src/survey/IPageDataCore.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IPageCore } from './IStepCore'; -import { IStepDataCore } from './IStepDataCore'; - -/** - * Data defintion for page step - */ -export interface IPageDataCore extends IStepDataCore { - step: IPageCore; -} diff --git a/packages/questionable-core/src/survey/IQuestionDataCore.ts b/packages/questionable-core/src/survey/IQuestionDataCore.ts deleted file mode 100644 index b22b552c..00000000 --- a/packages/questionable-core/src/survey/IQuestionDataCore.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IQuestionCore } from './IStepCore'; -import { IStepDataCore } from './IStepDataCore'; - -/** - * Data defintion for question step - */ - -export interface IQuestionDataCore extends IStepDataCore { - readonly core?: 'IQuestionData' | 'I'; - step: IQuestionCore; -} diff --git a/packages/questionable-core/src/survey/IQuestionableConfigCore.ts b/packages/questionable-core/src/survey/IQuestionableConfigCore.ts deleted file mode 100644 index 4c44a0e7..00000000 --- a/packages/questionable-core/src/survey/IQuestionableConfigCore.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { MODE } from '../util/enums'; -import { TGetDictionaryCore, TStringDictionaryCore } from '../util/types'; -import { IButtonCore } from './IButtonCore'; -import { IEventCore } from './IEventCore'; - -/** - * Configuration for customized behavior of Questionable - */ -export interface IQuestionableConfigCore { - /** - * Enables all developer tools (NOT for production use!) - * - * @title Developer Mode - * @default false - * @hidden - */ - readonly dev: boolean; - /** - * Event hooks for common form operations - * - * @title Events - * @hidden - */ - events?: Partial; - /** - * Optional method to fetch environment variables or query string parameters - * - * @title Get Runtime Config - * @hidden - */ - getRuntimeConfig?: TGetDictionaryCore; - /** - * View or edit mode - * - * @title Mode - * @default MODE.VIEW - */ - mode: MODE; - /** - * Navigation configuration - * - * @title Navigation - */ - nav: Partial; - /** - * Page configuration - * - * @title Pages - */ - pages: Partial; - /** - * Properties produced from `getRuntimeConfig()` - * @title Params - * @default {} - */ - get params(): TStringDictionaryCore; - /** - * Progress Bar configuration - * - * @title Progress Bar - */ - progressBar?: Partial; - /** - * Question configuration - * - * @title Question Configuration - */ - questions?: Partial; - - /** - * Step configuration - * - * @title Step Configuration - */ - steps?: Partial; -} - -/** - * Customizations for styling and formatting of the steps - */ -export interface IStepConfigCore { - readonly core?: 'IStepConfig' | 'I'; -} - -/** - * Configuration options for the progress bar - */ -export interface IProgressBarConfigCore { - readonly core?: 'IProgressBarConfig' | 'I'; -} - -/** - * Configuration for question display - */ -export interface IQuestionConfigCore { - readonly core?: 'IQuestionConfig' | 'I'; -} - -/** - * Configuration for buttons - */ -export interface IButtonConfigCore extends IButtonCore { - readonly core?: 'IButtonConfig' | 'I'; -} - -/** - * Configuration for navigation - */ -export interface INavigationConfigCore { - readonly core?: 'INavigationConfig' | 'I'; -} - -export interface IPageConfigCore { - readonly core?: 'IPageConfig' | 'I'; - visible?: boolean; -} - -export interface IPagesConfigCore { - readonly core?: 'IPagesConfig' | 'I'; -} diff --git a/packages/questionable-core/src/survey/IQuestionnaireCore.ts b/packages/questionable-core/src/survey/IQuestionnaireCore.ts deleted file mode 100644 index d519f646..00000000 --- a/packages/questionable-core/src/survey/IQuestionnaireCore.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IActionCore } from './IActionCore'; -import { IBranchCore } from './IBranchCore'; -import { IPagesCore } from './IPagesCore'; -import { IQuestionCore, ISectionCore } from './IStepCore'; -import { IQuestionableConfigCore } from './IQuestionableConfigCore'; -import { IResultCore } from './IResultCore'; - -/** - * Definition for survey data input - */ -export interface IQuestionnaireCore { - actions: IActionCore[]; - branches: IBranchCore[]; - config: IQuestionableConfigCore; - header: string; - pages: IPagesCore; - questions: IQuestionCore[]; - results: IResultCore[]; - sections: ISectionCore[]; -} diff --git a/packages/questionable-core/src/survey/IStepCore.ts b/packages/questionable-core/src/survey/IStepCore.ts deleted file mode 100644 index 59ee7948..00000000 --- a/packages/questionable-core/src/survey/IStepCore.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* eslint-disable import/no-cycle */ -import { - PAGE_TYPE, - PROGRESS_BAR_STATUS, - QUESTION_TYPE, - TStepType, -} from '../util/enums'; -import { TAgeCore, TAgeCalcCore } from '../util/types'; -import { IBranchCore } from './IBranchCore'; -import { IRefCore } from './IRefCore'; - -/** - * Acceptable responses - */ -export interface IResponseCore { - answers: Partial[]; - question: Partial; -} - -/** - * Generic step data definition. Applies to all types of steps. - */ -export interface IStepCore extends IRefCore { - /** - * Collection of requirements to view/enter this step - * - * @title Requirements - */ - entryRequirements?: IRequirementCore[]; - /** - * Collection of requirements to leave this step - * - * @title Exit Requirements - */ - exitRequirements?: IRequirementCore[]; - /** - * Optional footer text to display at the bottom of the step - * - * @title Footer - */ - footer?: string; - /** - * Contextual content to display below the step contents and above the footer - * - * @title Info - */ - info?: string; - /** - * Private/internal use only notes for documenting this step - * - * @title Internal Notes - */ - internalNotes?: string; - /** - * Display order of the Step. Determined at runtime. - * - * @title Order - * @hidden - */ - order?: number; - /** - * Section to which this step belongs - * - * @title Section - */ - section: Partial; - /** - * Text to display below the title - * - * @title Subtitle - */ - subTitle?: string; - /** - * Step's type, usually implemented by @see{IPageStep} or @see{IQuestionStep} - * - * @title Step Type - */ - type: TStepType; -} - -/** - * Defines step content for Question type - */ - -export interface IQuestionCore extends IStepCore { - /** - * The current answer for this question - * - * @title Answer - * @hidden Not viewable/editable in Design Mode - */ - answer?: string; - /** - * Collection of allowed answers - * - * @title Answers - */ - answers: IRefCore[]; - /** - * Collection of branches that use this question - * - * @title Branch - * @hidden - */ - branch?: Partial; - /** - * Type of question - * - * @title Question Type - */ - type: QUESTION_TYPE; -} - -/** - * Defines step content for Page types - */ - -export interface IPageCore extends IStepCore { - /** - * Defines the body content of the page - * - * @title Body - */ - body?: string; - /** - * Optional header to display above body - * - * @title Body Heading - */ - bodyHeader?: string; - /** - * Optional sub header to display below Body Heading - * - * @title Body Subheading - */ - bodySubHeader?: string; - /** - * Type of page - * - * @title Page Type - */ - type: PAGE_TYPE; -} - -/** - * Defines an individual requirement for accessing a step - */ -export interface IRequirementCore { - /** - * Optional, custom calculator for performing age-specific validation - * @hidden JSON schema does not support functions - */ - ageCalc?: TAgeCalcCore; - /** - * User facing description of this requirement - * - * @title Exlanation - */ - explanation?: string; - /** - * Optional maximum age allowed for this requirement - * - * @title Maximum Age - */ - maxAge?: TAgeCore; - /** - * Optional minimum age allowed for this requirement - * - * @title Minimum Age - */ - minAge?: TAgeCore; - /** - * Map of step id to required answer values - * - * @title Answers - */ - responses: IResponseCore[]; -} - -/** - * Defines a survey section, used in progress bar - */ -export interface ISectionCore extends IRefCore { - /** - * The last step id that is covered by this section - * - * @title Last Step - * @hidden Not viewable/editable in Design Mode - */ - lastStep?: number; - /** - * Collection of requirements to enable display of this status - * - * @title Requirements - */ - requirements: IRequirementCore[]; - /** - * Current display status of this section - * - * @title Status - * @hidden Not viewable/editable in Design Mode - */ - status?: PROGRESS_BAR_STATUS; -} diff --git a/packages/questionable-core/src/survey/IStepDataCore.ts b/packages/questionable-core/src/survey/IStepDataCore.ts deleted file mode 100644 index 84c2c8cb..00000000 --- a/packages/questionable-core/src/survey/IStepDataCore.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TReducerCore } from '../util/types'; -import { IFormCore } from './IFormCore'; -import { IStepCore } from './IStepCore'; - -/** - * Data defintion for base wizard step - */ -export interface IStepDataCore { - /** - * Reducer to execute with form progression - * - * @hidden JSON Schema doesn't support functions - */ - dispatchForm: TReducerCore; - /** - * The user's current form state - * - * @title FormCore - */ - form: IFormCore; - /** - * Current step - * - * @title Step - */ - step?: IStepCore; -/** - * Internally unique identifier - * - * @title Step ID - */ - stepId: string | number; -} diff --git a/packages/questionable-core/src/tests/Scaffolding.ts b/packages/questionable-core/src/tests/Scaffolding.ts new file mode 100644 index 00000000..3c74925c --- /dev/null +++ b/packages/questionable-core/src/tests/Scaffolding.ts @@ -0,0 +1,289 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable max-len */ +import { noop } from 'lodash'; +import { blue, red, white } from '../lib/logger'; +import { + ActionCore, +} from '../composable/ActionCore'; +import { SurveyBuilder } from '../constructable/SurveyBuilderCore'; +import { ResultCore } from '../composable/ResultCore'; +import { PagesCore } from '../composable/PagesCore'; +import { SectionCore } from '../composable/SectionCore'; +import { AnswerCore } from '../composable/AnswerCore'; +import { QuestionCore } from '../composable/QuestionCore'; +import { ResponseCore } from '../composable/ResponseCore'; +import { RequirementCore } from '../composable/RequirementCore'; +import { ACTION_TYPE } from '../metadata/properties/type/TActionType'; +import { PAGE_TYPE } from '../metadata/properties/type/TPageType'; +import { QUESTION_TYPE } from '../metadata/properties/type/TQuestionType'; + +export class Scaffolding { + // questionnaire: QuestionnaireCore; + + // form: FormCore; + + // iterable: Iterable; + + builder: SurveyBuilder; + + constructor() { + // eslint-disable-next-line no-multi-assign + this.builder = new SurveyBuilder(); + // this.iterable = new Iterable(this.questionnaire); + } + + build() { + const [onboarding] = this.builder.add(SectionCore, [ + { + requirements: [], + title: 'VA.gov Onboarding', + type: 'unlocked', + }, + ]); + this.builder.setDefaults(onboarding); + + this.builder.add(ActionCore, [ + { + label: 'Restart onboarding', + order: 1, + title: 'Restart', + type: ACTION_TYPE.NONE, + }, + ]); + const [finished] = this.builder.add(ActionCore, [ + { + title: 'Finished', + type: ACTION_TYPE.NONE, + }, + ]); + const [results] = this.builder.add(ResultCore, [ + { + action: finished, + label: 'Complete', + requirements: [], + title: 'Results', + type: 'match', + }, + ]); + this.builder.add(PagesCore, [ + { + landing: { + body: 'Please answer the following questions to setup your environment.', + id: PAGE_TYPE.LANDING, + section: onboarding, + title: + 'Welcome to the scaffolding project. Press any key to continue...', + type: PAGE_TYPE.LANDING, + }, + noResults: { + id: PAGE_TYPE.NO_RESULTS, + section: onboarding, + title: 'No actions have been performed', + type: PAGE_TYPE.NO_RESULTS, + }, + results: { + id: PAGE_TYPE.RESULTS, + section: onboarding, + title: 'Success. Your project has been bootstrapped.', + type: PAGE_TYPE.RESULTS, + }, + summary: { + id: PAGE_TYPE.SUMMARY, + section: onboarding, + title: + 'Review the output and confirm that everything was successful.', + type: PAGE_TYPE.SUMMARY, + }, + }, + ]); + const [YES, NO] = this.builder.add(AnswerCore, [ + { key: 'y', title: 'Yes' }, + { key: 'n', title: 'No' }, + ]); + const YES_NO = [YES, NO]; + + const [A] = this.builder.add(QuestionCore, [ + { + answers: YES_NO, + onAnswer: async () => noop(), + onDisplay: async () => { + blue( + 'This is the scaffolding project. You will be asked a series of questions which will guide you through the setup process.)', + ); + }, + title: + 'Is this your first time configuring this environment to run VA.gov?', + type: QUESTION_TYPE.MULTIPLE_CHOICE, + }, + ]); + + const [respondYes] = this.builder.add(ResponseCore, [ + { + answers: [YES], + question: A, + }, + ]); + const [respondYesOrNo] = this.builder.add(ResponseCore, [ + { + answers: [YES, NO], + question: A, + }, + ]); + const [isFirstTime] = this.builder.add(RequirementCore, [ + { + responses: [respondYes], + }, + ]); + const [hasAnsweredA] = this.builder.add(RequirementCore, [ + { + responses: [respondYesOrNo], + }, + ]); + + this.builder.add(QuestionCore, [ + { + answers: YES_NO, + componentType: 'path', + default: '', // os.homedir, + entryRequirements: [isFirstTime], + onAnswer: async (a: any) => { + const path = a.value || a.answer || a.short; + white(`Working directory has been set to ${path}`); + }, + onDisplay: async () => { + blue( + "Welcome aboard. We'll have you up and running in no time. The first step is to choose where your working directory is located. You can select either the current directory or you can specify your own path (note: this can be changed later, but it may be very time consuming)", + ); + }, + section: onboarding, + title: 'What directory do you want to use for this project?', + type: QUESTION_TYPE.TEXT, + validate: async (a: any) => { + const path = a.value || a.answer || a.short; + const exists = true; // fs.existsSync(`${path}`); + if (!exists) { + red(`"${path}" isn't a valid path. Please enter another path.`); + } + return exists; + }, + }, + ]); + + const repoChoices = this.builder.add(AnswerCore, [ + { + key: 'a', + short: 'all', + title: 'All', + }, + { + key: 'f', + short: 'front', + title: 'Front end', + }, + { + key: 'b', + short: 'backend', + title: 'Back end', + }, + { + key: 'c', + short: 'choose', + title: 'Let me choose', + }, + { + key: 'n', + short: 'none', + title: 'None; I will choose later', + }, + ]); + const [_, _b, _c, CHOOSE_REPOSITORIES, _e] = repoChoices; + const [C] = this.builder.add(QuestionCore, [ + { + answers: repoChoices, + entryRequirements: [hasAnsweredA], + id: 'C', + onAnswer: async (selected: any) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async () => { + blue( + 'The next step is to pull down the source code for the projects you will need to work on. You can have everything, just the frontend, just the backed or decide for each repo.', + ); + }, + title: 'Which repositories do you need?', + type: QUESTION_TYPE.MULTIPLE_CHOICE, + validate: async () => true, // fs.existsSync(`${path}`), + }, + ]); + + const repositories = this.builder.add( + AnswerCore, + [ + 'content-build', + 'digitalservice', + 'va-tools', + 'va.gov-team', + 'vagov-content', + 'veteran-facing-services-tools', + 'vets-api', + 'vets-api-mockdata', + 'vets-json-schema', + 'vets-website', + ].map((a) => ({ title: a })), + ); + + const [selectedChooseRepos] = this.builder.add(RequirementCore, [ + { + responses: this.builder.add(ResponseCore, [ + { + answers: [CHOOSE_REPOSITORIES], + question: C, + }, + ]), + }, + ]); + + this.builder.add(QuestionCore, [ + { + answers: repositories, + entryRequirements: [selectedChooseRepos], + onAnswer: async (selected: any) => { + white(`You selected ${selected.answer}`); + }, + onDisplay: async () => { + blue('Please select which repos you would like to clone.'); + }, + title: 'Which repositories do you need?', + type: QUESTION_TYPE.MULTIPLE_SELECT, + validate: async () => true, // fs.existsSync(`${path}`), + }, + ]); + + return [ + onboarding, + results, + + this.builder.add(ResultCore, [ + { + // action: { id: '0' }, + id: '1', + label: 'done', + requirements: this.builder.add(RequirementCore, [ + { + explanation: 'Scaffolding complete', + + // responses: [ + // { + // answers: [{ id: '1' }, { id: '0' }], + // question: { id: 'A' }, + // }, + // ], + }, + ]), + title: 'Completed Tasks', + }, + ]), + ]; + } +} diff --git a/packages/questionable-core/src/util/enums.ts b/packages/questionable-core/src/util/enums.ts deleted file mode 100644 index 11d537a9..00000000 --- a/packages/questionable-core/src/util/enums.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Defines the known component types for questions - */ -export enum QUESTION_TYPE { - DOB = 'dob', - MULTIPLE_CHOICE = 'multiple_choice', - MULTIPLE_SELECT = 'multiple_select', -} - -/** - * Defines the known component types for pages - */ -export enum PAGE_TYPE { - LANDING = 'Landing', - NO_RESULTS = 'No Results', - RESULTS = 'Results', - SUMMARY = 'Summary', -} - -/** - * Defines the known component types for design - */ -export enum DESIGN_TYPE { - EDIT = 'Edit', -} - -/** - * Defines the type of step for UI rendering - */ -export const STEP_TYPE = { ...PAGE_TYPE, ...QUESTION_TYPE, ...DESIGN_TYPE }; -export type TStepType = PAGE_TYPE | QUESTION_TYPE | DESIGN_TYPE; - -/** - * Navigation direction for steps by array index (+1 or -1) - */ -export enum DIRECTION { - FORWARD = 1, - BACKWARD = -1, -} - -/** - * Progress Bar status - */ -export enum PROGRESS_BAR_STATUS { - COMPLETE = 'complete', - CURRENT = 'current', - INCOMPLETE = 'incomplete', -} - -export enum ACTION { - CALL = 'call', - HYBRID = 'hybrid', - NONE = 'none', - ONLINE = 'online', -} - -export enum ACTION_TYPE { - RERENDER = 'RERENDER', - RESET = 'RESET', - UPDATE = 'UPDATE' -} - -export enum DATE_UNIT { - DAY = 'day', - MONTH = 'month', - YEAR = 'year', -} - -export enum MODE { - DEV = 'dev', - EDIT = 'edit', - VIEW = 'view', -} -// eslint-disable-next-line @typescript-eslint/ban-types -export const isEnum = (enm: object, value: string): boolean => - Object.values(enm).includes(value); diff --git a/packages/questionable-core/src/util/log.ts b/packages/questionable-core/src/util/log.ts deleted file mode 100644 index 3577d5cf..00000000 --- a/packages/questionable-core/src/util/log.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// import { serializeError } from 'serialize-error'; - -import { noop } from './noop'; - -const serializeError = noop; - -const { log: logInfo, error: logError } = console; - -export abstract class Logger { - /** - * Logs to the console. All arguments logged as an object. - * @param params - * @returns - */ - public static log(message: string, ...params: any) { - logInfo(message, ...params); - } - - public static error(message: any, e: Error, ...params: any) { - logError( - { - error: serializeError(e), - ...params, - }, - message, - ); - } -} - -export const { log, error } = Logger; diff --git a/packages/questionable-core/src/util/types.ts b/packages/questionable-core/src/util/types.ts deleted file mode 100644 index fe60d683..00000000 --- a/packages/questionable-core/src/util/types.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* - * Defines an age relative to a date - * @title Age Type - */ -export interface TAgeCore { - /** - * @minimum 0 - * @maximum 31 - * @nullable - * @title Days - */ - days?: number; - /** - * @minimum 0 - * @maximum 31 - * @nullable - * @title Months - */ - months: number; - /** - * @minimum 0 - * @maximum 100 - * @nullable - * @title Years - */ - years: number; -} - -/** - * Lambda that can be called to compute an age requirement - * @hidden - */ -export type TAgeCalcCore = (birthdate: string) => boolean; - -export type TReducerCore = (...params: any[]) => void; - -/** - * Key/value pairs which are both strings - */ -export type TStringDictionaryCore = { - [key: string]: string; -}; - -/** - * Generic fetch dictionary - */ -export type TGetDictionaryCore = (...params: any[]) => TStringDictionaryCore; - -export interface TDateOfBirthCore { - day?: string | undefined; - month?: string | undefined; - year?: string | undefined; -} - -export type TProgressBarTypeCore = string; - -export type TVerticalPositionCore = string; - -export type THorizontalPositionCore = string; - -export type TButtonModeCore = string; - -/** - * Content type for blocks of copy - */ -export interface TContentCore { - /** - * Main body content - * @title Content - */ - content?: string; - /** - * Text to display below the title - * @title Subtitle - */ - subTitle?: string; - /** - * Title or Header text - * @title Title - */ - title?: string; -} - -// OneOf is the main event: -// take a type T and a tuple type V, and return the type of -// T widened to relevant element(s) of V: -export type OneOf< - T, - V extends any[], - NK extends keyof V = Exclude - > = { [K in NK]: T extends V[K] ? V[K] : never }[NK]; diff --git a/packages/questionable-core/tsconfig.json b/packages/questionable-core/tsconfig.json index 9fe9276c..1fc216d4 100644 --- a/packages/questionable-core/tsconfig.json +++ b/packages/questionable-core/tsconfig.json @@ -42,7 +42,7 @@ } }, "include": [ - "src/**/*.ts" + "src/**/*.ts" ], "exclude": [ "node_modules", diff --git a/packages/questionable-mocks/typings.d.ts b/packages/questionable-mocks/typings.d.ts deleted file mode 100644 index 8cb29327..00000000 --- a/packages/questionable-mocks/typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.json'; diff --git a/packages/questionable-react-component/package.json b/packages/questionable-react-component/package.json index d3e16112..217e9cad 100644 --- a/packages/questionable-react-component/package.json +++ b/packages/questionable-react-component/package.json @@ -26,7 +26,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "semantic-ui-react": "^2.1.2", - "tslib": "^2.4.0", "typescript": "^4.6.3", "use-wizard": "^4.0.6" }, @@ -51,7 +50,6 @@ "@types/rimraf": "^3", "@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/parser": "^5.20.0", - "@usds.gov/questionable-build": "workspace:*", "babel-loader": "^8.2.4", "babel-preset-react-app": "^10.0.1", "eslint": "^8.13.0", @@ -113,7 +111,7 @@ "bundle": "yarn clean && yarn build && yarn build:rollup && yarn build:schema", "build:rollup": "rollup --config --environment NODE_ENV:production", "build:schemaJson": "ts-json-schema-generator -p './src/survey/*.ts' -o './dist/survey.json' --no-type-check --additional-properties true", - "build:schema": "yarn build:schemaJson && ts-node ./parseSchema.ts", + "build:schema": "yarn build:schemaJson && node ./parseSchema.mjs", "build:watch": "yarn build -w", "build:tsc": "tsc -p tsconfig.json", "clean": "rimraf ./dist", @@ -121,8 +119,8 @@ "type-check": "yarn build:tsc --noEmit", "deploy": "npm publish", "lint:fix": "pretty-quick --staged && yarn lint:ts --cache --fix", - "lint:ts": "eslint \"src/**/*.{ts,tsx}\"", - "pretty": "prettier \"src/**/*.{ts,tsx}\"" + "lint:ts": "eslint \"src/**/*.{ts,tsx,json,js}\"", + "pretty": "prettier \"src/**/*.{js,ts,tsx,css,json}\"" }, "type": "module", "version": "1.5.9" diff --git a/packages/questionable-react-component/parseSchema.ts b/packages/questionable-react-component/parseSchema.ts index 30671a64..f367b42f 100644 --- a/packages/questionable-react-component/parseSchema.ts +++ b/packages/questionable-react-component/parseSchema.ts @@ -1,7 +1,3 @@ import { parseSchema } from '@usds.gov/questionable-build'; -try { - parseSchema('./dist/survey.json', './src/schema/survey.ts'); -} catch (e) { - console.error(e); -} \ No newline at end of file +parseSchema('./dist/survey.json', './src/schema/survey.ts'); diff --git a/packages/questionable-react-component/scripts/parseSchema.js b/packages/questionable-react-component/scripts/parseSchema.js deleted file mode 100644 index c7bc3508..00000000 --- a/packages/questionable-react-component/scripts/parseSchema.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const fs = require('fs'); - -const rawdata = fs.readFileSync('./dist/survey.json'); - -const schema = `// This files is code generated. Do not edit. -/* eslint-disable */ -export const survey = ${rawdata};`; - -fs.writeFileSync('./src/schema/survey.ts', schema); diff --git a/packages/questionable-react-component/src/components/Questionable.tsx b/packages/questionable-react-component/src/components/Questionable.tsx index 3690fc6e..169f19fd 100644 --- a/packages/questionable-react-component/src/components/Questionable.tsx +++ b/packages/questionable-react-component/src/components/Questionable.tsx @@ -1,57 +1,50 @@ -import { useReducer } from 'react'; -import { useWizard } from 'use-wizard'; +import { useReducer } from 'react'; +import { useWizard } from 'use-wizard'; +import { + + FormCore, + GateLogicCore, + QuestionnaireCore, + defaultReducer, + eventedCore, +} from '@usds.gov/questionable-core'; import { CSS_CLASS } from '../lib/enums'; import { DevPanel } from './wizard/DevPanel'; -import { Form } from '../composable/Form'; import { GlobalStateProvider } from '../state/GlobalState'; -import { IQuestionable } from '../survey/IQuestionable'; import { ProgressFactory } from './factories/ProgressFactory'; import { StepFactory } from './factories/StepFactory'; -import { stepReducer } from '../state/stepReducer'; -import { IStepData } from '../survey'; -export const Questionable = ({ questionnaire }: IQuestionable): JSX.Element => { +type TQ = { + questionnaire: QuestionnaireCore, +}; +export const Questionable = ({ questionnaire }: TQ): JSX.Element => { if (!questionnaire) { throw new Error('questionable is undefined'); } - - const [step, wizard] = useWizard(questionnaire.flow); + const initForm = new FormCore(); // This is only used to store user inputs - const [form, dispatchForm] = useReducer(stepReducer, new Form()); - const props: IStepData = { - dispatchForm, - form, - stepId: step, - wizard, - }; - + const [form, dispatchForm] = useReducer(defaultReducer, initForm); + const gate = new GateLogicCore(questionnaire, form); + const [stepId, setStepId] = useWizard(gate.flow); + const step = gate.getStepById(`${stepId}`); + eventedCore.subscribe({ trigger: dispatchForm, type: 'reduce' }); return ( - +
- +
- +
- +
- +
diff --git a/packages/questionable-react-component/src/components/design/Edit.tsx b/packages/questionable-react-component/src/components/design/Edit.tsx index 3594851d..f86a7e84 100644 --- a/packages/questionable-react-component/src/components/design/Edit.tsx +++ b/packages/questionable-react-component/src/components/design/Edit.tsx @@ -4,20 +4,21 @@ import { kebabCase } from 'lodash'; // import { getStepSchema } from '../../schema/editStepSchema'; import { useGlobal } from '../../state/GlobalState'; -import { IPageData } from '../../survey/IPageData'; -import { IQuestionData } from '../../survey/IQuestionData'; +import { PageData } from '../../composable/PageData'; +import { QuestionData } from '../../composable/QuestionData'; import { Wizard } from '../lib'; import { DesignLayout } from '../wizard/DesignLayout'; +import { Step } from '../../composable/Step'; /** * Renders a question and a radio list of allowed answers * @param props * @returns */ -export const Edit = (_props: IQuestionData | IPageData): JSX.Element => { +export const Edit = ({step}: {step: Step}): JSX.Element => { const { questionnaire } = useGlobal(); //const schema = getStepSchema(props); - const fileName = kebabCase(questionnaire.header); + const fileName = kebabCase(questionnaire.questionnaire.header); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const onSubmit = ({ formData }: any) => { @@ -56,8 +57,8 @@ export const Edit = (_props: IQuestionData | IPageData): JSX.Element => { * @param props * @returns */ -export const EditStep = (props: IQuestionData | IPageData): JSX.Element => ( - - +export const EditStep = ({step}: {step: Step}): JSX.Element => ( + + ); diff --git a/packages/questionable-react-component/src/components/factories/DesignFactory.tsx b/packages/questionable-react-component/src/components/factories/DesignFactory.tsx index e56d1266..5e3455b1 100644 --- a/packages/questionable-react-component/src/components/factories/DesignFactory.tsx +++ b/packages/questionable-react-component/src/components/factories/DesignFactory.tsx @@ -1,28 +1,22 @@ import { isEnum, PAGE_TYPE, QUESTION_TYPE } from '@usds.gov/questionable-core'; import { noel } from '../../lib/noel'; import { useGlobal } from '../../state/GlobalState'; -import { IStep } from '../../survey/IStep'; -import { IStepData } from '../../survey/IStepData'; -import { IPageData } from '../../survey/IPageData'; -import { IQuestionData } from '../../survey/IQuestionData'; +import { Step } from '../../composable'; import { EditStep } from '../design/Edit'; /** * Given a step of a known question type, generates a question component * @param props * @returns */ -export const DesignFactory = (props: IStepData, step: IStep): JSX.Element => { - const { questionnaire } = useGlobal(); - const question = questionnaire.getStepById(step.id); - const stepData: IStepData = { ...{ step: question, ...props } }; +export const DesignFactory = ({ step }: {step: Step}): JSX.Element => { + const { questionnaire } = useGlobal(); + const question = questionnaire.getStepById(step.id); if (isEnum(QUESTION_TYPE, step.type)) { - const questionData = stepData as IQuestionData; - return ; + return ; } if (isEnum(PAGE_TYPE, step.type)) { - const pageData = stepData as IPageData; - return ; + return ; } return noel('Not an editable type'); }; diff --git a/packages/questionable-react-component/src/components/factories/PageFactory.tsx b/packages/questionable-react-component/src/components/factories/PageFactory.tsx index df890234..c35676c6 100644 --- a/packages/questionable-react-component/src/components/factories/PageFactory.tsx +++ b/packages/questionable-react-component/src/components/factories/PageFactory.tsx @@ -1,40 +1,40 @@ import { + GateLogicCore, isEnum, PAGE_TYPE, } from '@usds.gov/questionable-core'; -import { noel } from '../../lib/noel'; -import { useGlobal } from '../../state/GlobalState'; -import { IStepData } from '../../survey/IStepData'; -import { IPageData } from '../../survey/IPageData'; +import { noel } from '../../lib/noel'; +import { useGlobal } from '../../state/GlobalState'; +import { Step, Page } from '../../composable'; import { LandingPage, NoResultsPage, ResultsPage, SummaryPage, } from '../pages'; +import { PageComposer } from '../lib/Pages'; /** * Given a step of a known page type, returns a page component * @param props * @returns */ -export const PageFactory = (props: IStepData): JSX.Element => { - const { stepId } = props; +export const PageFactory = ({ step, gate }: {gate: GateLogicCore, step: Step}): JSX.Element => { const { questionnaire } = useGlobal(); - const step = questionnaire.getStepById(`${stepId}`); - if (!isEnum(PAGE_TYPE, step.type)) { + const s = questionnaire.getStepById(step.id); + if (!isEnum(PAGE_TYPE, s.type)) { return noel('Not a page'); } - const page = questionnaire.getPageById(step.id); - const stepData = { ...{ step: page, ...props } } as IPageData; + const page = questionnaire.getPageById(step.id) as Page; + const comp = new PageComposer({ gate, page }); switch (page.type) { case PAGE_TYPE.LANDING: - return ; + return ; case PAGE_TYPE.NO_RESULTS: - return ; + return ; case PAGE_TYPE.RESULTS: - return ; + return ; case PAGE_TYPE.SUMMARY: - return ; + return ; default: return noel('Page does not exist', 'PageFactory'); } diff --git a/packages/questionable-react-component/src/components/factories/ProgressFactory.tsx b/packages/questionable-react-component/src/components/factories/ProgressFactory.tsx index e262e147..c90c2dde 100644 --- a/packages/questionable-react-component/src/components/factories/ProgressFactory.tsx +++ b/packages/questionable-react-component/src/components/factories/ProgressFactory.tsx @@ -3,10 +3,10 @@ import { ProgressBar } from '../wizard/ProgressBar'; import { StepIndicator } from '../wizard/StepIndicator'; import { noel } from '../../lib/noel'; import { useGlobal } from '../../state/GlobalState'; -import { IStepData } from '../../survey/IStepData'; +import { Step } from '../../composable'; -export const ProgressFactory = ({ props, position }: - { position: TVerticalPositionCore, props: IStepData }): JSX.Element => { +export const ProgressFactory = ({ step, position }: + { position: TVerticalPositionCore, step: Step }): JSX.Element => { const { config } = useGlobal(); if (config.progressBar.hide || config.progressBar.position !== position) { @@ -14,9 +14,9 @@ export const ProgressFactory = ({ props, position }: } switch (config.progressBar.type) { case 'progress-bar': - return ; + return ; case 'step-indicator': - return ; + return ; default: return noel('Could not find progress type', config.progressBar.type); } diff --git a/packages/questionable-react-component/src/components/factories/QuestionFactory.tsx b/packages/questionable-react-component/src/components/factories/QuestionFactory.tsx index dbd9b675..afaa2ee5 100644 --- a/packages/questionable-react-component/src/components/factories/QuestionFactory.tsx +++ b/packages/questionable-react-component/src/components/factories/QuestionFactory.tsx @@ -1,38 +1,38 @@ import { + GateLogicCore, isEnum, QUESTION_TYPE, } from '@usds.gov/questionable-core'; -import { noel } from '../../lib/noel'; -import { useGlobal } from '../../state/GlobalState'; -import { IStepData } from '../../survey/IStepData'; -import { IQuestionData } from '../../survey/IQuestionData'; +import { noel } from '../../lib/noel'; +import { useGlobal } from '../../state/GlobalState'; +import { Question, Step } from '../../composable'; import { DateOfBirthStep, MultipleChoiceStep, MultiSelectStep, } from '../questions'; +import { QuestionComposer } from '../lib/Questions'; /** * Given a step of a known question type, generates a question component * @param props * @returns */ -export const QuestionFactory = (props: IStepData): JSX.Element => { - const { stepId } = props; +export const QuestionFactory = ({ step, gate }: {gate: GateLogicCore, step: Step}): JSX.Element => { const { questionnaire } = useGlobal(); - const step = questionnaire.getStepById(`${stepId}`); - if (!isEnum(QUESTION_TYPE, step.type)) { return noel('Not a question'); } - const question = questionnaire.getQuestionById(step.id); - const stepData = { ...{ step: question, ...props } } as IQuestionData; + const question = questionnaire.getQuestionById(step.id) as Question; + // const stepData = { ...{ step: question, ...props } } as Partial; + const comp = new QuestionComposer({ gate, question }); + switch (question.type) { case QUESTION_TYPE.DOB: - return ; + return ; case QUESTION_TYPE.MULTIPLE_CHOICE: - return ; + return ; case QUESTION_TYPE.MULTIPLE_SELECT: - return ; + return ; default: return noel('Question does not exist', 'QuestionFactory'); } diff --git a/packages/questionable-react-component/src/components/factories/StepFactory.tsx b/packages/questionable-react-component/src/components/factories/StepFactory.tsx index b7453f6e..60dc4d78 100644 --- a/packages/questionable-react-component/src/components/factories/StepFactory.tsx +++ b/packages/questionable-react-component/src/components/factories/StepFactory.tsx @@ -1,20 +1,20 @@ import { + GateLogicCore, isEnum, MODE, PAGE_TYPE, QUESTION_TYPE, } from '@usds.gov/questionable-core'; import { noel } from '../../lib/noel'; import { useGlobal } from '../../state/GlobalState'; -import { IStep } from '../../survey/IStep'; -import { IStepData } from '../../survey/IStepData'; +import { Step } from '../../composable'; import { DesignFactory } from './DesignFactory'; import { PageFactory } from './PageFactory'; import { QuestionFactory } from './QuestionFactory'; -const viewFactory = (props: IStepData, step: IStep): JSX.Element => { +const viewFactory = ({ step, gate }: {gate: GateLogicCore, step: Step}): JSX.Element => { if (isEnum(QUESTION_TYPE, step.type)) { - return QuestionFactory(props); + return QuestionFactory({ gate, step }); } if (isEnum(PAGE_TYPE, step.type)) { - return PageFactory(props); + return PageFactory({ gate, step }); } return noel('Step does not exist', 'StepFactory'); }; @@ -24,13 +24,12 @@ const viewFactory = (props: IStepData, step: IStep): JSX.Element => { * @param props * @returns */ -export const StepFactory = (props: IStepData): JSX.Element => { - const { stepId } = props; +export const StepFactory = ({ step, gate }: {gate: GateLogicCore, step: Step}): JSX.Element => { const { questionnaire, config } = useGlobal(); - const step = questionnaire.getStepById(`${stepId}`); + const s = questionnaire.getStepById(step.id); if (config.mode === MODE.EDIT) { - return DesignFactory(props, step); + return DesignFactory({ step: s }); } - return viewFactory(props, step); + return viewFactory({ gate, step: s }); }; diff --git a/packages/questionable-react-component/src/components/lib/Pages.tsx b/packages/questionable-react-component/src/components/lib/Pages.tsx index 4a19c8aa..8251c5b8 100644 --- a/packages/questionable-react-component/src/components/lib/Pages.tsx +++ b/packages/questionable-react-component/src/components/lib/Pages.tsx @@ -1,51 +1,67 @@ -import { kebabCase } from 'lodash'; -import { ReactNode } from 'react'; -import { groupBy } from '@usds.gov/questionable-core'; -import { CSS_CLASS } from '../../lib/enums'; -import { Div } from '../factories/NodeFactory'; -import { IGlobalState } from '../../state/GlobalState'; -import { IResult } from '../../survey/IResult'; -import { IStepData } from '../../survey/IStepData'; -import { setResults } from '../../state/persists'; -import { TResultData } from '../../survey/IEvent'; +/* eslint-disable import/no-cycle */ +/* eslint-disable max-classes-per-file */ +import { kebabCase } from 'lodash'; +import { ReactNode } from 'react'; +import { + groupBy, + GateLogicCore, + QuestionableConfigCore, + QuestionnaireCore, + ResultCore, +} from '@usds.gov/questionable-core'; +import { CSS_CLASS } from '../../lib/enums'; +import { Div } from '../factories/NodeFactory'; +import { setResults } from '../../state/persists'; +import { Page } from '../../composable'; + +export class PageComposer { + page!: Page; + + config!: QuestionableConfigCore; + + questionnaire!: QuestionnaireCore; + + gate!: GateLogicCore; + + constructor({ + page, gate, + }: { + gate: GateLogicCore, page: Page, + }) { + this.page = page; + this.gate = gate; + this.questionnaire = gate.questionnaire; + this.config = gate.config; + } -/** - * Static utility methods for page components - */ -export abstract class Pages { /** * Internal method to compute reason for a result * @param props * @param result * @returns */ - static getReason( - props: IStepData, - result: IResult, - global: IGlobalState, - ): string { - let reason = result.match?.explanation; - const { questionnaire, config } = global; + getReason({ result }:{result: ResultCore}): string { + let reason = result.match?.explanation; if (!reason) { return ''; } - if (config?.dev && result.match) { + if (this.config?.dev && result.match) { reason += '

'; if ( result.match.ageCalc !== undefined || result.match.minAge !== undefined || result.match.maxAge !== undefined ) { - reason += `You are ${props.form.age?.years} years `; - reason += `and ${props.form.age?.months} months old. `; + reason += `You are ${this.gate.form.age?.years} years `; + reason += `and ${this.gate.form.age?.months} months old. `; } result.match.responses.forEach((r) => { - if (!r.question.id) { + if (!r.question?.id) { return; } - const q = questionnaire.getQuestionById(r.question.id); + const q = this.gate.getQuestionById(r.question.id); reason += `You answered "${q.answer}" to the question "${q.title}." `; }); } @@ -57,22 +73,21 @@ export abstract class Pages { * @param props * @returns */ - static getResults(props: IStepData, global: IGlobalState): ReactNode { - const { questionnaire } = global; - const data: TResultData = { - props, - results: questionnaire.getResults(props.form).map((result) => ({ + getResults(): ReactNode { + const data = { + page: this.page, + results: this.gate.getResults().map((result) => ({ category: result.category, id: result.id, label: result.label, - reason: Pages.getReason(props, result, global), + reason: this.getReason({ result }), title: result.title, })), step: 'results', }; setResults( - kebabCase(questionnaire.header), + kebabCase(this.questionnaire.header), data.results.map((r) => ({ description: r.reason, name: r.title, @@ -85,7 +100,7 @@ export abstract class Pages { const cat = categories[key]; const group = cat.map((result) => (
  • diff --git a/packages/questionable-react-component/src/components/lib/Questions.tsx b/packages/questionable-react-component/src/components/lib/Questions.tsx index 9a5cb470..89834414 100644 --- a/packages/questionable-react-component/src/components/lib/Questions.tsx +++ b/packages/questionable-react-component/src/components/lib/Questions.tsx @@ -1,43 +1,79 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable max-classes-per-file */ /* eslint-disable no-param-reassign */ -import { Checkbox, Fieldset, Radio } from '@trussworks/react-uswds'; -import { QuestionsCore } from '@usds.gov/questionable-core'; -import { QuestionableConfig } from '../../composable/QuestionableConfig'; -import { IQuestionData } from '../../survey/IQuestionData'; -import { IRef } from '../../survey/IRef'; -import { Steps } from './Steps'; -import { CSS_CLASS } from '../../lib/enums'; - -/** - * Static utility methods for question components - */ -export abstract class Questions extends QuestionsCore { +import { + Checkbox, + Fieldset, + Radio, +} from '@trussworks/react-uswds'; +import { + GateLogicCore, + QuestionableConfigCore, + QuestionnaireCore, + TStepReducerAction, + TDateOfBirthCore, + FormCore, + updateForm, + isSelected, + getBirthdate, + toBirthdate, +} from '@usds.gov/questionable-core'; +import { Question } from '../../composable'; +import { CSS_CLASS } from '../../lib/enums'; + +export class QuestionComposer { + question!: Question; + + config!: QuestionableConfigCore; + + questionnaire!: QuestionnaireCore; + + gate!: GateLogicCore; + + constructor({ + question, gate, + }: { + gate: GateLogicCore, question: Question, + }) { + this.question = question; + this.gate = gate; + this.questionnaire = gate.questionnaire; + this.config = gate.config; + } + + public updateForm(title: string) { + return updateForm({ answer: title, form: this.gate.form, question: this.question }); + } + + public isSelected(title: string) { + return isSelected({ answer: title, form: this.gate.form, question: this.question }) === true; + } + /** * Generates a radio button given a question definition * @param answer * @param props * @returns */ - private static getRadio( - answer: IRef, - props: IQuestionData, - config: QuestionableConfig, + private getRadio( + { answer }: {answer: {title: string}}, ): JSX.Element { - const title = Questions.getString(answer); - const handler = () => Questions.updateForm(title, props, config); - const id = Steps.getDomId(title, props); + const { title } = answer; + const handler = () => this.updateForm(title); + const id = this.question.getDomId(title); return ( ); } @@ -47,17 +83,14 @@ export abstract class Questions extends QuestionsCore { * @param props * @returns */ - public static getRadios( - props: IQuestionData, - config: QuestionableConfig, - ): JSX.Element { + public getRadios(): JSX.Element { return (
    - {props.step.answers.map((a) => Questions.getRadio(a, props, config))} + {this.question.answers.map((answer) => this.getRadio({ answer }))}
    ); } @@ -68,27 +101,25 @@ export abstract class Questions extends QuestionsCore { * @param props * @returns */ - protected static getCheckbox( - answer: IRef, - props: IQuestionData, - config: QuestionableConfig, + protected getCheckbox( + { answer }: {answer: {title: string}}, ): JSX.Element { - const title = Questions.getString(answer); - const handler = () => Questions.updateForm(title, props, config); - const id = Steps.getDomId(title, props); + const { title } = answer; + const handler = () => this.updateForm(title); + const id = this.question.getDomId(title); return ( ); } @@ -98,18 +129,27 @@ export abstract class Questions extends QuestionsCore { * @param props * @returns */ - public static getCheckboxes( - props: IQuestionData, - config: QuestionableConfig, - ): JSX.Element { + public getCheckboxes(): JSX.Element { return (
    - {props.step.answers.map((a) => Questions.getCheckbox(a, props, config))} + {this.question.answers.map((answer) => this.getCheckbox({ answer }))}
    ); } + + public getBirthdate() { + return getBirthdate({ form: this.gate.form, question: this.question }); + } + + public toBirthdate(dob: TDateOfBirthCore) { + return toBirthdate({ dob, question: this.question }); + } + + public dispatchForm(action: TStepReducerAction) { + return FormCore.reducer(this.gate.form, action); + } } diff --git a/packages/questionable-react-component/src/components/lib/Steps.tsx b/packages/questionable-react-component/src/components/lib/Steps.tsx index bc5b4d08..29503672 100644 --- a/packages/questionable-react-component/src/components/lib/Steps.tsx +++ b/packages/questionable-react-component/src/components/lib/Steps.tsx @@ -1,8 +1,7 @@ -import { StepsCore } from '@usds.gov/questionable-core'; -import { IStepData } from '../../survey/IStepData'; +import { TWizard } from '../../composable/Wizard'; -export abstract class Steps extends StepsCore { - public static goToStep(step: string, props: IStepData): void { - props.wizard.goToStep(step); +export abstract class Steps { + public static goToStep({ step, wizard }: {step: string, wizard: TWizard}): void { + wizard?.goToStep(step); } } diff --git a/packages/questionable-react-component/src/components/lib/Wizard.tsx b/packages/questionable-react-component/src/components/lib/Wizard.tsx index 1116fade..11412513 100644 --- a/packages/questionable-react-component/src/components/lib/Wizard.tsx +++ b/packages/questionable-react-component/src/components/lib/Wizard.tsx @@ -1,66 +1,64 @@ -import { SiteAlert } from '@trussworks/react-uswds'; -import FileSaver from 'file-saver'; -import { ACTION_TYPE } from '@usds.gov/questionable-core'; -import { QuestionableConfig } from '../../composable/QuestionableConfig'; -import { CSS_CLASS } from '../../lib/enums'; -import { noel } from '../../lib/noel'; -import { IStepData } from '../../survey/IStepData'; -import { Span } from '../factories/NodeFactory'; +import { SiteAlert } from '@trussworks/react-uswds'; +import FileSaver from 'file-saver'; +import { ACTION_TYPE, FormCore, QuestionableConfigCore } from '@usds.gov/questionable-core'; +import { CSS_CLASS } from '../../lib/enums'; +import { noel } from '../../lib/noel'; +import { Span } from '../factories/NodeFactory'; +import { Step } from '../../composable'; +import { TWizard } from '../../composable/Wizard'; export abstract class Wizard { - public static getCssClass( - prefix: CSS_CLASS, + public static getCssClass({ prefix, name, step }: { name: string, - props: IStepData, - ): string { + prefix: CSS_CLASS, + step: Step, + }): string { const base = `${prefix}-${name}`; return [ `${base}`, - `${base}-${props.step?.type}`, - `${base}-${props.stepId}`, + `${base}-${step?.type}`, + `${base}-${step.id}`, ].join(' '); } - public static getHeader( - props: IStepData, - config: QuestionableConfig, - ): JSX.Element { - let text = props.step?.title; + public static getHeader({ step, config }: + {config: QuestionableConfigCore, step: Step }): JSX.Element { + let text = step?.title; if (!text) { return noel(); } if (config.steps.showStepId) { - text = `${props.step?.id}: ${text}`; + text = `${step?.id}: ${text}`; } return (

    {text}

    ); } - public static getSubtitle(props: IStepData): JSX.Element { - const text = props.step?.subTitle; + public static getSubtitle({ step }: {step: Step}): JSX.Element { + const text = step?.subTitle; if (!text) { return noel(); } return ( ); } - public static getInfoBox(props: IStepData): JSX.Element { - const text = props.step?.info; + public static getInfoBox({ step }: {step: Step}): JSX.Element { + const text = step?.info; if (!text) { return noel(); } @@ -68,39 +66,40 @@ export abstract class Wizard { {text} ); } - public static getFooter(props: IStepData): JSX.Element { - const text = props.step?.footer; + public static getFooter({ step }: {step: Step}): JSX.Element { + const text = step?.footer; if (!text) { return noel(); } return ( ); } - public static resetQuestionable(props: IStepData): void { - props.dispatchForm({ - type: ACTION_TYPE.RESET, + public static resetQuestionable({ form, wizard }:{form: FormCore, wizard: TWizard}): void { + FormCore.reducer(form, { + type: ACTION_TYPE.RESET, + value: '', }); - props.wizard.goToStep('A'); + wizard.goToStep('A'); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any diff --git a/packages/questionable-react-component/src/components/lib/types.ts b/packages/questionable-react-component/src/components/lib/types.ts new file mode 100644 index 00000000..f7a9644b --- /dev/null +++ b/packages/questionable-react-component/src/components/lib/types.ts @@ -0,0 +1,20 @@ +/* eslint-disable import/no-cycle */ +import { GateLogicCore, QuestionableConfigCore, QuestionnaireCore } from '@usds.gov/questionable-core'; +import { QuestionComposer } from './Questions'; +import { PageComposer } from './Pages'; +import { + QuestionData, StepData, PageData, Page, Question, Step, +} from '../../composable'; + +export type TQstn = { + comp: QuestionComposer | PageComposer; + props?: Partial | Partial; + step: Step | Question | Page; +}; + +export type TComp = { + config: QuestionableConfigCore; + gate: GateLogicCore; + props: Partial | Partial | Partial; + questionnaire: QuestionnaireCore; +}; diff --git a/packages/questionable-react-component/src/components/pages/LandingPage.tsx b/packages/questionable-react-component/src/components/pages/LandingPage.tsx index 01ffa6dc..ca91e4ea 100644 --- a/packages/questionable-react-component/src/components/pages/LandingPage.tsx +++ b/packages/questionable-react-component/src/components/pages/LandingPage.tsx @@ -1,21 +1,24 @@ -import { noel } from '../../lib/noel'; -import { IPageData } from '../../survey/IPageData'; -import { Span } from '../factories/NodeFactory'; -import { StepLayout } from '../wizard/StepLayout'; +import { Page } from '../../composable/Page'; +import { noel } from '../../lib/noel'; +import { Span } from '../factories/NodeFactory'; +import { PageComposer } from '../lib/Pages'; +import { StepLayout } from '../wizard/StepLayout'; /** * Generates the first/initial/landing page of the Wizard * @param props * @returns */ -export const LandingPage = (props: IPageData): JSX.Element => { - const { step } = props; +export const LandingPage = ({ step, comp }: { + comp: PageComposer, + step: Page +}): JSX.Element => { if (!step) { return noel(); } return ( - + diff --git a/packages/questionable-react-component/src/components/pages/NoResultsPage.tsx b/packages/questionable-react-component/src/components/pages/NoResultsPage.tsx index af61885c..44e9a258 100644 --- a/packages/questionable-react-component/src/components/pages/NoResultsPage.tsx +++ b/packages/questionable-react-component/src/components/pages/NoResultsPage.tsx @@ -1,32 +1,35 @@ -import { ACTION } from '@usds.gov/questionable-core'; -import { CSS_CLASS } from '../../lib/enums'; -import { Action } from '../wizard/Action'; -import { IPageData } from '../../survey/IPageData'; -import { noel } from '../../lib/noel'; -import { Span } from '../factories/NodeFactory'; -import { StepLayout } from '../wizard/StepLayout'; -import { useGlobal } from '../../state/GlobalState'; +import { ACTION, GateLogicCore } from '@usds.gov/questionable-core'; +import { CSS_CLASS } from '../../lib/enums'; +import { Action } from '../wizard/Action'; +import { noel } from '../../lib/noel'; +import { Span } from '../factories/NodeFactory'; +import { StepLayout } from '../wizard/StepLayout'; +import { useGlobal } from '../../state/GlobalState'; +import { PageComposer } from '../lib'; +import { Page } from '../../composable'; /** * Displays the wizard results * @param props * @returns */ -export const NoResultsPage = (props: IPageData): JSX.Element => { - const { step } = props; - const global = useGlobal(); - const { questionnaire, config } = global; +export const NoResultsPage = ({ step, comp, gate }: { + comp: PageComposer, + gate: GateLogicCore, + step: Page +}): JSX.Element => { + const { questionnaire, config } = useGlobal(); if (!step) { return noel(); } - config.events.noResult(props.form); + config.events.noResult(gate.form); const action = questionnaire.getActionByType(ACTION.NONE); return ( - + { /> - + ); }; diff --git a/packages/questionable-react-component/src/components/pages/ResultsPage.tsx b/packages/questionable-react-component/src/components/pages/ResultsPage.tsx index 6e2fd2fc..fdddd480 100644 --- a/packages/questionable-react-component/src/components/pages/ResultsPage.tsx +++ b/packages/questionable-react-component/src/components/pages/ResultsPage.tsx @@ -1,27 +1,30 @@ -import { Action } from '../wizard/Action'; -import { CSS_CLASS } from '../../lib'; -import { IPageData } from '../../survey/IPageData'; -import { noel } from '../../lib/noel'; -import { Pages } from '../lib'; -import { Span } from '../factories/NodeFactory'; -import { StepLayout } from '../wizard/StepLayout'; -import { useGlobal } from '../../state/GlobalState'; +import { GateLogicCore } from '@usds.gov/questionable-core'; +import { Action } from '../wizard/Action'; +import { CSS_CLASS } from '../../lib'; +import { noel } from '../../lib/noel'; +import { Span } from '../factories/NodeFactory'; +import { StepLayout } from '../wizard/StepLayout'; +import { useGlobal } from '../../state/GlobalState'; +import { Page } from '../../composable'; +import { PageComposer } from '../lib'; /** * Displays the wizard results * @param props * @returns */ -export const ResultsPage = (props: IPageData): JSX.Element => { - const { step } = props; - const global = useGlobal(); - const { questionnaire, config } = global; +export const ResultsPage = ({ step, comp, gate }: { + comp: PageComposer, + gate: GateLogicCore, + step: Page +}): JSX.Element => { + const { questionnaire, config } = useGlobal(); if (!step) { return noel(); } - const results = questionnaire.getResults(props.form); + const results = questionnaire.getResults(); const action = questionnaire.getAction(results); const secondaryActions = results .filter((r) => r.secondaryAction) @@ -31,17 +34,15 @@ export const ResultsPage = (props: IPageData): JSX.Element => { followupActions = (
    {secondaryActions.map((a) => ( - + ))}
    ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const genResults = results as any; - config.events.onResults({ ...props.form, results: genResults }); + config.events.onResults({ results, step }); return ( - +
    {
      - {Pages.getResults(props, global)} + {comp.getResults()}
    - + {followupActions}
    diff --git a/packages/questionable-react-component/src/components/pages/SummaryPage.tsx b/packages/questionable-react-component/src/components/pages/SummaryPage.tsx index 40578b80..c68d8529 100644 --- a/packages/questionable-react-component/src/components/pages/SummaryPage.tsx +++ b/packages/questionable-react-component/src/components/pages/SummaryPage.tsx @@ -1,30 +1,37 @@ -import { ReactNode } from 'react'; -import { CSS_CLASS } from '../../lib/enums'; -import { IPageData } from '../../survey/IPageData'; -import { IQuestion } from '../../survey'; -import { noel } from '../../lib/noel'; -import { StepLayout } from '../wizard/StepLayout'; -import { Steps } from '../lib'; /* eslint-disable no-script-url */ +import { ReactNode } from 'react'; +import { GateLogicCore } from '@usds.gov/questionable-core'; +import { CSS_CLASS } from '../../lib/enums'; +import { Question } from '../../composable/Question'; +import { noel } from '../../lib/noel'; +import { StepLayout } from '../wizard/StepLayout'; +import { Step } from '../../composable'; +import { PageComposer } from '../lib'; +type tGa = { + comp: PageComposer, + gate: GateLogicCore, + onClick?: (question: Question) => void, + step: Step +} /** * Internal method to generate a list of the survey answers * @param props * @returns */ -const getAnswers = (props: IPageData, onClick: (question: IQuestion) => void): ReactNode => { - const answers = props.form.responses.map((question, i) => ( +const getAnswers = ({ onClick, gate }: tGa): ReactNode => { + const answers = gate.form.responses.map((question, i) => (
  • ${question.title}` }} onClick={() => { - onClick(question); + if (onClick) onClick(question); return false; }} onKeyDown={() => { - onClick(question); + if (onClick) onClick(question); return false; }} > @@ -43,16 +50,16 @@ const getAnswers = (props: IPageData, onClick: (question: IQuestion) => void): R * @param props * @returns */ -export const SummaryPage = (props: IPageData): JSX.Element => { - const { step: page } = props; - - if (!page) { +export const SummaryPage = ({ step, gate, comp }: tGa): JSX.Element => { + if (!step) { return noel(); } - const onClick = (question: IQuestion) => { - Steps.goToStep(question.id, props); + const onClick = (question: Question) => { + gate.goToStep(question); }; - return {getAnswers(props, onClick)}; + return {getAnswers({ + comp, gate, onClick, step, + })}; }; diff --git a/packages/questionable-react-component/src/components/questions/DateOfBirth.tsx b/packages/questionable-react-component/src/components/questions/DateOfBirth.tsx index 9cde123c..eea67347 100644 --- a/packages/questionable-react-component/src/components/questions/DateOfBirth.tsx +++ b/packages/questionable-react-component/src/components/questions/DateOfBirth.tsx @@ -5,20 +5,19 @@ import { import { TDateOfBirthCore } from '@usds.gov/questionable-core'; import { noel } from '../../lib/noel'; import { useGlobal } from '../../state/GlobalState'; -import { IQuestionData } from '../../survey/IQuestionData'; -import { Questions } from '../lib/Questions'; import { StepLayout } from '../wizard/StepLayout'; import { TDoBUtilParams, IInfoBox, getDateInputGroup, } from './lib/DateOfBirthUtils'; +import { Question } from '../../composable'; +import { QuestionComposer } from '../lib'; -export const DateOfBirth = (props: IQuestionData): JSX.Element => { - const { config, questionnaire } = useGlobal(); - const { step } = props; - const birthdate = Questions.getBirthdate(props); - const dob: TDateOfBirthCore = { +export const DateOfBirth = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => { + const { questionnaire } = useGlobal(); + const birthdate = comp.getBirthdate(); + const dob: TDateOfBirthCore = { day: birthdate?.day?.toString(), month: birthdate?.month?.toString(), year: birthdate?.year?.toString(), @@ -27,7 +26,7 @@ export const DateOfBirth = (props: IQuestionData): JSX.Element => { const [state, setState] = useState(dob); const startMessage: IInfoBox = { message: '', type: 'info' }; const [error, setError] = useState(startMessage); - const [cookieName, setCookieName] = useState(kebabCase(questionnaire.header)); + const [cookieName, setCookieName] = useState(kebabCase(questionnaire.questionnaire.header)); if (!step) { return noel(); @@ -41,11 +40,11 @@ export const DateOfBirth = (props: IQuestionData): JSX.Element => { setState, state, }; - return getDateInputGroup('date_of_birth', props, config, params); + return getDateInputGroup('date_of_birth', step, params, comp); }; -export const DateOfBirthStep = (props: IQuestionData): JSX.Element => ( - - +export const DateOfBirthStep = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => ( + + ); diff --git a/packages/questionable-react-component/src/components/questions/MultiSelect.tsx b/packages/questionable-react-component/src/components/questions/MultiSelect.tsx index eee3caff..0b03cf6d 100644 --- a/packages/questionable-react-component/src/components/questions/MultiSelect.tsx +++ b/packages/questionable-react-component/src/components/questions/MultiSelect.tsx @@ -1,22 +1,18 @@ -import { noel } from '../../lib/noel'; -import { useGlobal } from '../../state/GlobalState'; -import { IQuestionData } from '../../survey/IQuestionData'; -import { Questions } from '../lib/Questions'; -import { StepLayout } from '../wizard/StepLayout'; +import { noel } from '../../lib/noel'; +import { StepLayout } from '../wizard/StepLayout'; +import { QuestionComposer } from '../lib/Questions'; +import { Question } from '../../composable/Question'; /** * Renders a question and a checkbox list of allowed answers * @param props * @returns */ -export const MultiSelect = (props: IQuestionData): JSX.Element => { - const { config } = useGlobal(); - - if (props?.step?.answers === undefined) { +export const MultiSelect = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => { + if (step?.answers === undefined) { return noel(); } - - return Questions.getCheckboxes(props, config); + return comp.getCheckboxes(); }; /** @@ -24,8 +20,8 @@ export const MultiSelect = (props: IQuestionData): JSX.Element => { * @param props * @returns */ -export const MultiSelectStep = (props: IQuestionData): JSX.Element => ( - - +export const MultiSelectStep = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => ( + + ); diff --git a/packages/questionable-react-component/src/components/questions/MultipleChoice.tsx b/packages/questionable-react-component/src/components/questions/MultipleChoice.tsx index cb43e66c..f51ff3d8 100644 --- a/packages/questionable-react-component/src/components/questions/MultipleChoice.tsx +++ b/packages/questionable-react-component/src/components/questions/MultipleChoice.tsx @@ -1,31 +1,27 @@ -import { noel } from '../../lib/noel'; -import { useGlobal } from '../../state'; -import { IQuestionData } from '../../survey/IQuestionData'; -import { Questions } from '../lib/Questions'; -import { StepLayout } from '../wizard/StepLayout'; +import { Question } from '../../composable/Question'; +import { noel } from '../../lib/noel'; +import { QuestionComposer } from '../lib/Questions'; +import { StepLayout } from '../wizard/StepLayout'; /** * Renders a question and a radio list of allowed answers - * @param props + * @param step * @returns */ -export const MultipleChoice = (props: IQuestionData): JSX.Element => { - const { config } = useGlobal(); - - if (props?.step?.answers === undefined) { +export const MultipleChoice = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => { + if (step?.answers === undefined) { return noel('Question and answer are not defined'); } - - return Questions.getRadios(props, config); + return comp.getRadios(); }; /** * Renders a question and a radio list of allowed answers - * @param props + * @param step * @returns */ -export const MultipleChoiceStep = (props: IQuestionData): JSX.Element => ( - - +export const MultipleChoiceStep = ({ step, comp }: {comp: QuestionComposer, step: Question}): JSX.Element => ( + + ); diff --git a/packages/questionable-react-component/src/components/questions/lib/DateOfBirthUtils.tsx b/packages/questionable-react-component/src/components/questions/lib/DateOfBirthUtils.tsx index c40c8013..9a684cec 100644 --- a/packages/questionable-react-component/src/components/questions/lib/DateOfBirthUtils.tsx +++ b/packages/questionable-react-component/src/components/questions/lib/DateOfBirthUtils.tsx @@ -12,13 +12,11 @@ import { getAge, TDateOfBirthCore, } from '@usds.gov/questionable-core'; -import { noel } from '../../../lib/noel'; -import { QuestionableConfig } from '../../../composable'; -import { setAge } from '../../../state/persists'; -import { IQuestionData } from '../../../survey/IQuestionData'; -import { Questions } from '../../lib/Questions'; -import { Steps } from '../../lib/Steps'; -import { CSS_CLASS } from '../../../lib/enums'; +import { noel } from '../../../lib/noel'; +import { setAge } from '../../../state/persists'; +import { CSS_CLASS } from '../../../lib/enums'; +import { Question } from '../../../composable'; +import { QuestionComposer } from '../../lib'; type TInfoBox = 'error' | 'warning' | 'info'; @@ -112,9 +110,9 @@ export const onDoBKeyPress = ( export const onDateOfBirthChange = ( e: ChangeEvent, unit: DATE_UNIT, - props: IQuestionData, - config: QuestionableConfig, + question: Question, utilParams: TDoBUtilParams, + comp: QuestionComposer, // eslint-disable-next-line sonarjs/cognitive-complexity ): void => { const { @@ -137,7 +135,7 @@ export const onDateOfBirthChange = ( setState({ ...state, }); - const bd = Questions.toBirthdate(state); + const bd = comp.toBirthdate(state); const age = getAge(bd); const monthIsValid = isValid(DATE_UNIT.MONTH, state[DATE_UNIT.MONTH] || ''); const dayIsValid = isValid(DATE_UNIT.DAY, state[DATE_UNIT.DAY] || ''); @@ -146,20 +144,20 @@ export const onDateOfBirthChange = ( if (age && bd) { setError({ message: '', type: 'info' }); setAge(cookieName, age.years); - props.dispatchForm({ + comp.dispatchForm({ type: ACTION_TYPE.UPDATE, value: { age, birthdate: bd, }, }); - Questions.updateForm(bd, props, config); - if (props.step.exitRequirements && age.years > 0) { - const invalid = props.step.exitRequirements.every( + comp.updateForm(bd); + if (question.exitRequirements && age.years > 0) { + const invalid = question.exitRequirements.every( (r) => r.minAge && age.years < r.minAge.years, ); if (invalid) { - const min = props.step.exitRequirements + const min = question.exitRequirements .map((r) => r.minAge?.years) .join(', '); // eslint-disable-next-line max-len @@ -170,7 +168,7 @@ export const onDateOfBirthChange = ( } } } else if ((monthIsValid || dayIsValid || yearIsValid) - || (props.form?.age?.years && props.form.age.years > 0)) { + || (comp.gate.form?.age?.years && comp.gate.form.age.years > 0)) { let text = ''; if ((yearIsValid && !monthIsValid) || (dayIsValid && !monthIsValid)) { text += ' month'; @@ -190,7 +188,7 @@ export const onDateOfBirthChange = ( message: `"${message.join(' ')}`, type: 'error', }); - props.dispatchForm({ + comp.dispatchForm({ type: ACTION_TYPE.UPDATE, value: { age: { years: 0 }, @@ -203,9 +201,9 @@ export const onDateOfBirthChange = ( export const getDateInput = ( unit: DATE_UNIT, label: string, - props: IQuestionData, - config: QuestionableConfig, + question: Question, utilParams: TDoBUtilParams, + comp: QuestionComposer, ): JSX.Element => { const { state } = utilParams; let disabled = true; @@ -242,7 +240,7 @@ export const getDateInput = ( return ( onDateOfBirthChange(e, unit, props, config, utilParams)} + onChange={(e) => onDateOfBirthChange(e, unit, question, utilParams, comp)} onKeyPress={(e) => onDoBKeyPress(e, unit)} /> ); @@ -282,15 +280,15 @@ export const getInfoBox = (utilParams: TDoBUtilParams): JSX.Element => { export const getDateInputGroup = ( label: string, - props: IQuestionData, - config: QuestionableConfig, + question: Question, utilParams: TDoBUtilParams, + comp: QuestionComposer, ): JSX.Element => (
    - - {getDateInput(DATE_UNIT.MONTH, label, props, config, utilParams)} - {getDateInput(DATE_UNIT.DAY, label, props, config, utilParams)} - {getDateInput(DATE_UNIT.YEAR, label, props, config, utilParams)} + + {getDateInput(DATE_UNIT.MONTH, label, question, utilParams, comp)} + {getDateInput(DATE_UNIT.DAY, label, question, utilParams, comp)} + {getDateInput(DATE_UNIT.YEAR, label, question, utilParams, comp)} {getInfoBox(utilParams)}
    diff --git a/packages/questionable-react-component/src/components/wizard/Action.tsx b/packages/questionable-react-component/src/components/wizard/Action.tsx index fc3f7c23..2d3aeb27 100644 --- a/packages/questionable-react-component/src/components/wizard/Action.tsx +++ b/packages/questionable-react-component/src/components/wizard/Action.tsx @@ -1,15 +1,13 @@ -import { Link } from '@trussworks/react-uswds'; -import { CSS_CLASS } from '../../lib'; -import { noel } from '../../lib/noel'; -import { useGlobal } from '../../state'; -import { IForm } from '../../survey'; -import { IAction } from '../../survey/IAction'; -import { H2, H3, Span } from '../factories/NodeFactory'; +import { Link } from '@trussworks/react-uswds'; +import { ActionCore, FormCore } from '@usds.gov/questionable-core'; +import { CSS_CLASS } from '../../lib'; +import { noel } from '../../lib/noel'; +import { useGlobal } from '../../state'; +import { H2, H3, Span } from '../factories/NodeFactory'; export const Action = ({ action, page }: - { action: Partial, page: IForm }): JSX.Element => { - const global = useGlobal(); - const { config } = global; + { action: Partial, page: FormCore }): JSX.Element => { + const { config } = useGlobal(); const buttons = action.buttons?.map((a) => { if (!a.link) { @@ -29,7 +27,7 @@ export const Action = ({ action, page }: variant={variant} href={a.link} onClick={() => { - config.events.action({ ...page, ...action }); + config.events.action(page); }} > {a.title} diff --git a/packages/questionable-react-component/src/components/wizard/Button.tsx b/packages/questionable-react-component/src/components/wizard/Button.tsx index 0ae6f386..94bdd831 100644 --- a/packages/questionable-react-component/src/components/wizard/Button.tsx +++ b/packages/questionable-react-component/src/components/wizard/Button.tsx @@ -8,8 +8,7 @@ import { import { CSS_CLASS } from '../../lib/enums'; import { noel } from '../../lib/noel'; import { useGlobal } from '../../state/GlobalState'; -import { IStepData } from '../../survey/IStepData'; -import { Steps } from '../lib/Steps'; +import { Step } from '../../composable/Step'; type TButtonConfig = { dir: 'next' | 'prev'; @@ -20,87 +19,81 @@ type TButtonConfig = { stepId: string; }; -const Button = (props: TButtonConfig): JSX.Element => ( +const Button = ({ + dir, disabled, label, mode, onClick, stepId, +}: TButtonConfig): JSX.Element => ( - {props.label} + {label} ); -interface INavBar extends IStepData { +type INavBar = { + step: Step; verticalPos: TVerticalPositionCore; -} +}; -export const PreviousButton = (props: INavBar): JSX.Element => { +export const PreviousButton = ({ step, verticalPos }: INavBar): JSX.Element => { const { questionnaire, config } = useGlobal(); - if (config.nav.prev.visible === false) { + if (config.nav?.prev?.visible === false) { return noel(); } - - const { step } = props; - if (step?.buttons?.prev?.visible === false) { return noel(); } - const layoutMismatch = props.verticalPos !== config.nav.prev.verticalPos; - const surveyStart = (props.stepId === STEP_TYPE.LANDING + const layoutMismatch = verticalPos !== config.nav?.prev?.verticalPos; + const surveyStart = (step.type === STEP_TYPE.LANDING && questionnaire.flow[0] === STEP_TYPE.LANDING) - || props.stepId === questionnaire.flow[0]; - const surveyEnd = props.stepId === STEP_TYPE.RESULTS - || props.stepId === STEP_TYPE.NO_RESULTS; + || step.id === questionnaire.flow[0]; + const surveyEnd = step.type === STEP_TYPE.RESULTS + || step.type === STEP_TYPE.NO_RESULTS; const notEditMode = config.mode !== MODE.EDIT - || (props.stepId === questionnaire.flow[0] && config.mode === MODE.EDIT); + || (step.id === questionnaire.flow[0] && config.mode === MODE.EDIT); const doNotRender = layoutMismatch || ((surveyStart || surveyEnd) && notEditMode); if (doNotRender) { return noel(); } - const label = step?.buttons?.prev?.title || config.nav.prev.defaultLabel || 'Previous'; - const onClick = () => Steps.goToPrevStep(props, questionnaire); + const label = step?.buttons?.prev?.title || config.nav.prev?.defaultLabel || 'Previous'; + const onClick = () => questionnaire.goToPrevStep(step); const disabled = () => false; - return ( - + + + + ); +} + +export const AppContainer = (): JSX.Element => { + const eligibility = buildEligibility({}); + const args = { + questionnaire: new Questionnaire(eligibility), + }; + + return ( +
    + +
    + ); +}; + +/** + * Flatten the data returned from the API before passing it on + * @param data + * @returns + */ +const transformDataToCMS = (data: any) => { + let json: Partial = {}; + if (data?.data !== undefined && data?.data?.length > 0) { + try { + const questions = data?.data?.map((question: Datum) => { + const q = question.attributes; + q.id = q.question_id; + return q; + }).reduce((ret: IQuestionData, question: Attributes) => { + if (question.id) { + ret[question.id] = question; + return ret; + } return ret; + }, {}); + json = merge(json, { questions }); + } catch (e) { + handleErrors('There was an error parsing the API response.', e); + } + } + return json as CMS; +}; + +/** + * React application container for the web component + * @param config - object representing the Fetch configuration + * @returns + */ +const Container = () => { + return AppContainer(); +}; + +export const App = (config: IFetchAPI = { + url: API_URL, +}) => ( + { + handleErrors('An error occurred', e); + }} + > + + +); diff --git a/packages/ssa-eligibility/package.json b/packages/ssa-eligibility/package.json index 327c05b1..4a30a8b3 100644 --- a/packages/ssa-eligibility/package.json +++ b/packages/ssa-eligibility/package.json @@ -49,7 +49,6 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", "gh-pages": "^3.2.3", - "pretty-quick": "^3.1.3", "react-docgen-typescript-loader": "^3.7.2", "rollup": "^2.70.2", "rollup-plugin-copy": "^3.4.0", @@ -60,7 +59,7 @@ "typescript": "^4.6.3" }, "engines": { - "node": "14.x - 18.x" + "node": "14.x - 16.x" }, "files": [ "dist", @@ -80,8 +79,8 @@ "questionnaire" ], "license": "ISC", - "main": "dist/index.js", - "module": "dist/index.esm.js", + "main": "dist/main.js", + "module": "dist/main.esm.js", "name": "@usds.gov/ssa-eligibility", "scripts": { "rs:build": "react-scripts build", @@ -101,6 +100,6 @@ "lint:ts": "eslint \"src/**/*.{ts,tsx,json,js}\"", "pretty": "prettier \"src/**/*.{js,ts,tsx,css,json}\"" }, - "types": "dist/index.d.ts", + "types": "dist/main.d.ts", "version": "1.1.13" } diff --git a/packages/ssa-eligibility/src/App.tsx b/packages/ssa-eligibility/src/App.tsx index 4b6add55..7b42ab3e 100644 --- a/packages/ssa-eligibility/src/App.tsx +++ b/packages/ssa-eligibility/src/App.tsx @@ -3,8 +3,10 @@ import { noop, Questionable, - Questionnaire, } from '@usds.gov/questionable-react-component'; +import { + Questionnaire, +} from '@usds.gov/questionable-core'; import { isEmpty, merge } from 'lodash'; import { useFetch } from 'react-async'; import { ErrorBoundary } from 'react-error-boundary'; @@ -43,7 +45,7 @@ export const AppContainer = (data: any = {}): JSX.Element => { } const gtag = window.gtag || noop; gtag('config', 'ssa_eligibility_wizard', { - send_page_view: false, + send_page_view: false, // eslint-disable-line camelcase }); const args = { questionnaire: new Questionnaire(eligibility), diff --git a/packages/ssa-eligibility/src/index.ts b/packages/ssa-eligibility/src/main.ts similarity index 100% rename from packages/ssa-eligibility/src/index.ts rename to packages/ssa-eligibility/src/main.ts diff --git a/packages/ssa-eligibility/tests/setupTests.ts b/packages/ssa-eligibility/tests/setupTests.ts index 8af8cc8a..df71ab50 100644 --- a/packages/ssa-eligibility/tests/setupTests.ts +++ b/packages/ssa-eligibility/tests/setupTests.ts @@ -6,3 +6,4 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +import '@testing-library/jest-dom/extend-expect'; diff --git a/types.d.ts b/types.d.ts deleted file mode 100644 index 353c3332..00000000 --- a/types.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/triple-slash-reference */ -/// - -declare namespace NodeJS { - interface ProcessEnv { - readonly NODE_ENV: 'development' | 'production' | 'test'; - readonly PUBLIC_URL: string; - } -} - -declare module '*.json' { - const src: string; - export default src; -} - -/* Markdown */ -declare module '*.md'; diff --git a/yarn.lock b/yarn.lock index fef442cf..3eb4c1d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5955,7 +5955,6 @@ __metadata: eslint-plugin-yaml: ^0.5.0 prettier: ^2.6.2 prettier-plugin-organize-imports: ^2.3.4 - pretty-quick: ^3.1.3 rimraf: ^3.0.2 rollup: ^2.70.2 rollup-plugin-copy: ^3.4.0 @@ -6096,7 +6095,6 @@ __metadata: short-unique-id: ^4.4.4 ts-json-schema-generator: ^1.0.0 ts-node: ^10.7.0 - tslib: ^2.4.0 typescript: ^4.6.3 uuid: ^8.3.2 languageName: unknown @@ -6178,7 +6176,6 @@ __metadata: "@types/rimraf": ^3 "@typescript-eslint/eslint-plugin": ^5.20.0 "@typescript-eslint/parser": ^5.20.0 - "@usds.gov/questionable-build": "workspace:*" "@usds.gov/questionable-core": "workspace:*" babel-loader: ^8.2.4 babel-preset-react-app: ^10.0.1 @@ -6221,7 +6218,6 @@ __metadata: semantic-ui-react: ^2.1.2 ts-json-schema-generator: ^1.0.0 ts-node: ^10.7.0 - tslib: ^2.4.0 typescript: ^4.6.3 use-wizard: ^4.0.6 languageName: unknown @@ -6321,7 +6317,6 @@ __metadata: eslint-plugin-react-hooks: ^4.4.0 gh-pages: ^3.2.3 lodash: ^4.17.21 - pretty-quick: ^3.1.3 react: ^17.0.2 react-async: ^10.0.1 react-docgen-typescript-loader: ^3.7.2 @@ -24160,7 +24155,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1": version: 2.4.0 resolution: "tslib@npm:2.4.0" checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113