├── .nvmrc ├── .prettierrc.json ├── designer ├── .pa11yci.json ├── snowpack.config.js ├── __mocks__ │ ├── styleMock.js │ └── imageMock.js ├── client │ ├── i18n │ │ ├── index.ts │ │ └── translations │ │ │ └── cy.translation.json │ ├── data │ │ ├── section │ │ │ ├── index.ts │ │ │ ├── addSection.ts │ │ │ └── __tests__ │ │ │ │ └── addSection.jest.ts │ │ ├── list │ │ │ ├── index.ts │ │ │ ├── addList.ts │ │ │ ├── findList.ts │ │ │ └── __tests__ │ │ │ │ ├── findList.jest.ts │ │ │ │ └── addList.jest.ts │ │ ├── component │ │ │ ├── index.ts │ │ │ └── addComponent.ts │ │ ├── condition │ │ │ ├── index.ts │ │ │ ├── hasConditions.ts │ │ │ ├── addCondition.ts │ │ │ ├── removeCondition.ts │ │ │ └── __tests__ │ │ │ │ └── hasConditions.jest.ts │ │ ├── page │ │ │ ├── index.ts │ │ │ ├── addPage.ts │ │ │ ├── allPathsLeadingTo.ts │ │ │ ├── findPage.ts │ │ │ ├── updateLinksTo.ts │ │ │ ├── __tests__ │ │ │ │ └── addPage.jest.ts │ │ │ └── updateLink.ts │ │ └── index.ts │ ├── components │ │ ├── Page │ │ │ └── index.ts │ │ ├── Flyout │ │ │ └── index.ts │ │ ├── BackLink │ │ │ ├── index.ts │ │ │ ├── BackLink.tsx │ │ │ └── BackLink.scss │ │ ├── Menu │ │ │ ├── index.ts │ │ │ ├── useTabs.tsx │ │ │ └── useMenuItem.tsx │ │ ├── CssClasses │ │ │ └── index.ts │ │ ├── FormDetails │ │ │ ├── index.ts │ │ │ ├── FormDetails.scss │ │ │ └── FormDetailsTitle.tsx │ │ ├── PageLinkage │ │ │ └── index.ts │ │ ├── Autocomplete │ │ │ └── index.ts │ │ ├── ErrorMessage │ │ │ ├── index.ts │ │ │ ├── ErrorMessage.tsx │ │ │ └── __tests__ │ │ │ │ └── ErrorMessage.jest.tsx │ │ ├── Visualisation │ │ │ ├── index.ts │ │ │ └── Info.tsx │ │ ├── ComponentCreate │ │ │ ├── index.ts │ │ │ ├── ComponentCreate.scss │ │ │ └── __tests__ │ │ │ │ └── ComponentCreateList.jest.tsx │ │ ├── RenderInPortal │ │ │ ├── index.ts │ │ │ └── RenderInPortal.tsx │ │ ├── CustomValidationMessage │ │ │ └── index.ts │ │ ├── Icons │ │ │ ├── index.ts │ │ │ ├── ChevronRightIcon.tsx │ │ │ ├── MoveUpIcon.tsx │ │ │ ├── MoveDownIcon.tsx │ │ │ └── SearchIcon.tsx │ │ └── FieldEditors │ │ │ ├── email-edit.tsx │ │ │ └── list-field-edit.tsx │ ├── hooks │ │ ├── list │ │ │ └── useListItem │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ ├── featureToggling.tsx │ │ └── __tests__ │ │ │ └── FeatureTogglingHook.jest.tsx │ ├── pages │ │ ├── ErrorPages │ │ │ ├── index.ts │ │ │ └── ErrorPage.scss │ │ └── LandingPage │ │ │ ├── index.ts │ │ │ └── LandingPage.scss │ ├── styles │ │ ├── colors.scss │ │ └── _utils.scss │ ├── context │ │ ├── index.ts │ │ ├── FlyoutContext.ts │ │ └── DataContext.ts │ ├── FeatureToggle.tsx │ ├── conditions │ │ ├── inline-condition-helpers.js │ │ ├── TextValues.tsx │ │ └── __tests__ │ │ │ └── inline-condition-helpers.jest.ts │ ├── reducers │ │ ├── component │ │ │ └── index.ts │ │ └── listActions.tsx │ ├── __tests__ │ │ ├── page-edit.jest.tsx │ │ ├── helpers │ │ │ └── mocks.ts │ │ └── helpers.jest.tsx │ ├── randomId.ts │ ├── api │ │ ├── toggleApi.ts │ │ └── designerApi.ts │ ├── __mocks__ │ │ └── tabbable.js │ ├── modal.js │ ├── outputs │ │ └── webhook-edit.tsx │ ├── plugins │ │ └── logger.ts │ └── load-form-configurations.js ├── .gitignore ├── postcss.config.js ├── nodemon.json ├── server │ ├── views │ │ ├── split.html │ │ ├── includes │ │ │ ├── home-office-header.html │ │ │ └── home-office-footer.html │ │ └── designer.html │ ├── plugins │ │ ├── routes │ │ │ ├── index.ts │ │ │ └── healthCheck.ts │ │ ├── logging.ts │ │ ├── session.ts │ │ ├── blankie.ts │ │ └── router.ts │ ├── index.ts │ ├── lib │ │ ├── publish │ │ │ └── index.ts │ │ └── persistence │ │ │ ├── blobPersistenceService.ts │ │ │ ├── index.ts │ │ │ └── persistenceService.ts │ └── __tests__ │ │ └── healthCheck.jest.ts ├── jest-server-setup.js ├── bin │ └── symlink-config ├── LICENSE ├── jest-setup.js ├── jest.server.config.js ├── test │ ├── helpers │ │ ├── sub-component-assertions.js │ │ └── react-testing-library-utils.ts │ ├── testServer.js │ └── .setup.js ├── .eslintrc.js ├── tsconfig.json └── new-form.json ├── runner ├── Procfile ├── src │ ├── server │ │ ├── schemas │ │ │ └── index.ts │ │ ├── plugins │ │ │ ├── engine │ │ │ │ ├── models │ │ │ │ │ ├── Section.ts │ │ │ │ │ ├── RepeatingSummaryViewModel.ts │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── summaryViewModel.jest.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── submission │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── SummaryViewModel.detailsTransformationMap.ts │ │ │ │ │ └── FormModel.exitOptions.ts │ │ │ │ ├── components │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── InsetText.ts │ │ │ │ │ ├── RadiosField.ts │ │ │ │ │ ├── Para.ts │ │ │ │ │ ├── Details.ts │ │ │ │ │ ├── SelectField.ts │ │ │ │ │ ├── Html.ts │ │ │ │ │ └── FlashCard.ts │ │ │ │ ├── views │ │ │ │ │ ├── components │ │ │ │ │ │ ├── html.html │ │ │ │ │ │ ├── para.html │ │ │ │ │ │ ├── datefield.html │ │ │ │ │ │ ├── numberfield.html │ │ │ │ │ │ ├── timefield.html │ │ │ │ │ │ ├── datetimefield.html │ │ │ │ │ │ ├── textfield.html │ │ │ │ │ │ ├── websitefield.html │ │ │ │ │ │ ├── yesnofield.html │ │ │ │ │ │ ├── radiosfield.html │ │ │ │ │ │ ├── emailaddressfield.html │ │ │ │ │ │ ├── selectfield.html │ │ │ │ │ │ ├── telephonenumberfield.html │ │ │ │ │ │ ├── checkboxesfield.html │ │ │ │ │ │ ├── datepartsfield.html │ │ │ │ │ │ ├── monthyearfield.html │ │ │ │ │ │ ├── datetimepartsfield.html │ │ │ │ │ │ ├── insettext.html │ │ │ │ │ │ ├── details.html │ │ │ │ │ │ ├── multilinetextfield.html │ │ │ │ │ │ ├── fileuploadfield.html │ │ │ │ │ │ └── list.html │ │ │ │ │ ├── partials │ │ │ │ │ │ ├── conditional-components.html │ │ │ │ │ │ ├── components.html │ │ │ │ │ │ └── heading.html │ │ │ │ │ └── index.html │ │ │ │ ├── feedback │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ └── FeedbackContextInfo.ts │ │ │ │ ├── pluginHandlers │ │ │ │ │ ├── files │ │ │ │ │ │ └── prehandlers │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── getFiles.ts │ │ │ │ │ └── exit │ │ │ │ │ │ └── prehandlers │ │ │ │ │ │ ├── getBacklink.ts │ │ │ │ │ │ ├── getForm.ts │ │ │ │ │ │ ├── parseExitEmailErrors.ts │ │ │ │ │ │ └── getState.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pageControllers │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── MultiStartPageController.ts │ │ │ │ │ ├── HomePageController.ts │ │ │ │ │ ├── StartPageController.ts │ │ │ │ │ └── StartDatePageController.ts │ │ │ │ └── services │ │ │ │ │ └── configurationService.ts │ │ │ ├── initialiseSession │ │ │ │ ├── index.ts │ │ │ │ ├── configurePlugin.ts │ │ │ │ └── types.ts │ │ │ ├── pulse.ts │ │ │ ├── session.ts │ │ │ ├── applicationStatus │ │ │ │ ├── checkUserCompletedSummary.ts │ │ │ │ └── retryPay.ts │ │ │ ├── logging.ts │ │ │ └── rateLimit.ts │ │ ├── routes │ │ │ ├── index.ts │ │ │ ├── health-check.ts │ │ │ └── public.ts │ │ ├── services │ │ │ ├── upload │ │ │ │ ├── index.ts │ │ │ │ └── mockUploadService.ts │ │ │ ├── payService.nanoid.ts │ │ │ ├── index.ts │ │ │ └── QueueService.ts │ │ ├── config.ts │ │ ├── utils │ │ │ ├── generateCookiePassword.ts │ │ │ └── url.ts │ │ ├── forms │ │ │ └── README.md │ │ ├── templates │ │ │ └── additionalContexts.json │ │ ├── views │ │ │ ├── help │ │ │ │ ├── privacy.html │ │ │ │ └── accessibility-statement.html │ │ │ ├── 500.html │ │ │ ├── application-error.html │ │ │ ├── mini-summary.html │ │ │ ├── partials │ │ │ │ ├── summary-detail.html │ │ │ │ └── summary-row.html │ │ │ └── timeout.html │ │ └── transforms │ │ │ └── summaryDetails │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── client │ │ └── sass │ │ │ ├── _govuk.scss │ │ │ ├── _upload-dialog.scss │ │ │ └── _hmpo.scss │ └── index.ts ├── bin │ ├── build │ ├── build-css │ ├── run │ │ └── check │ │ │ ├── index.js │ │ │ ├── util.js │ │ │ ├── getJsonFiles.js │ │ │ ├── getOutOfDateForms.js │ │ │ └── check.js │ └── symlink-config ├── nodemon.json ├── performance │ └── dummy.pdf ├── public │ └── static │ │ ├── favicon.ico │ │ ├── upload-dialog.js │ │ └── object-from-entries-polyfill.js ├── test │ ├── cases │ │ └── server │ │ │ ├── dummy.pdf │ │ │ ├── forms │ │ │ ├── phase-default.json │ │ │ ├── phase-none.json │ │ │ └── phase-alpha.json │ │ │ ├── utils │ │ │ └── generateCookiePassword.test.ts │ │ │ ├── plugins │ │ │ └── engine │ │ │ │ ├── EmailAddressField.test.ts │ │ │ │ ├── feedback-context-info.test.ts │ │ │ │ └── services │ │ │ │ └── configurationService.test.ts │ │ │ ├── services │ │ │ └── httpService.test.ts │ │ │ └── rate-limit.test.js │ └── html-helper.js ├── config │ ├── development.json │ ├── production.json │ └── test.json ├── .gitignore ├── LICENSE ├── .babelrc └── tsconfig.json ├── .browserslistrc ├── .eslintignore ├── model ├── src │ ├── schema │ │ └── index.ts │ ├── form │ │ ├── index.ts │ │ └── form-configuration.ts │ ├── migration │ │ ├── index.ts │ │ ├── types.ts │ │ ├── __tests__ │ │ │ └── whichMigrations.jest.ts │ │ ├── whichMigrations.ts │ │ └── migration.0-2.ts │ ├── components │ │ ├── index.ts │ │ └── conditional-component-types.ts │ ├── data-model │ │ ├── index.ts │ │ └── __tests__ │ │ │ └── isMultipleApiKey.test.ts │ ├── conditions │ │ ├── helpers.ts │ │ ├── condition-value-registration.ts │ │ ├── index.ts │ │ ├── condition-group-def.ts │ │ ├── types.ts │ │ ├── condition-field.ts │ │ └── condition-value-abstract.ts │ ├── index.ts │ └── utils │ │ ├── logger.ts │ │ └── helpers.ts ├── LICENSE ├── tsconfig.json └── jest.config.js ├── submitter ├── config │ ├── test.json │ ├── custom-environment-variables.json │ └── default.js ├── src │ ├── config.ts │ ├── submission │ │ ├── services │ │ │ └── index.ts │ │ ├── retention │ │ │ └── errors.ts │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── logging.ts │ │ │ ├── retentionCron.ts │ │ │ ├── retention.ts │ │ │ └── poll.ts │ │ ├── types.ts │ │ └── setupDatabase.ts │ └── __mocks__ │ │ └── prismaClient.ts ├── nodemon.json ├── jest.config.js ├── .babelrc └── tsconfig.json ├── e2e ├── cypress │ ├── fixtures │ │ ├── passes.png │ │ ├── fails-ocr.png │ │ ├── example.json │ │ └── image-quality-playback.json │ ├── support │ │ ├── step_definitions │ │ │ ├── common │ │ │ │ ├── i_reload.js │ │ │ │ ├── i_go_back.js │ │ │ │ ├── i_choose_string.js │ │ │ │ ├── i_continue.js │ │ │ │ ├── i_enter_string.js │ │ │ │ ├── i_wait.js │ │ │ │ ├── i_click_the_back_link.js │ │ │ │ ├── i_select_the_option_string.js │ │ │ │ ├── i_click_the_link_string.js │ │ │ │ ├── i_see_the_heading_string.js │ │ │ │ ├── i_submit_the_form.js │ │ │ │ ├── i_see_the_path_is_string.js │ │ │ │ ├── i_dont_see_the_page_string.js │ │ │ │ ├── i_select_string_for_string.js │ │ │ │ ├── i_have_read_the_disclaimer.js │ │ │ │ ├── i_dont_see_string.js │ │ │ │ ├── i_enter_string_for_string.js │ │ │ │ ├── i_expand_string_to_see_string.js │ │ │ │ ├── i_select.js │ │ │ │ ├── i_see_string.js │ │ │ │ ├── i_choose_string_for_string.js │ │ │ │ ├── i_see_the_error_string_for_string.js │ │ │ │ ├── i_edit_page_string.js │ │ │ │ └── i_enter_the_date_string_in_parts_for_string.js │ │ │ ├── runner │ │ │ │ ├── i_am_redirected_to_string.js │ │ │ │ ├── i_am_viewing_the_runner_at_string.js │ │ │ │ ├── i_navigate_to_string.js │ │ │ │ ├── i_navigate_to_the_string_form.js │ │ │ │ ├── i_upload_a_file_that_string.js │ │ │ │ ├── the_field_string_contains_string.js │ │ │ │ ├── i_see_the_string_page.js │ │ │ │ ├── i_see_a_summary_list_with_the_values.js │ │ │ │ └── the_form_string_exists.js │ │ │ └── designer │ │ │ │ ├── i_am_viewing_the_designer.js │ │ │ │ ├── i_navigate_away_from_the_designer.js │ │ │ │ ├── i_delete_the_page.js │ │ │ │ ├── i_see_the_section_title_string_is_displayed_in_the_preview.js │ │ │ │ ├── i_am_on_the_new_configuration_page.js │ │ │ │ ├── i_change_the_page_path_to_string.js │ │ │ │ ├── i_will_see_an_alert_warning_me_that_string.js │ │ │ │ ├── i_change_the_page_title_to_string.js │ │ │ │ ├── i_create_a_section_titled_string.js │ │ │ │ └── i_preview_the_page_string.js │ │ └── e2e.js │ └── e2e │ │ ├── runner │ │ ├── files.js │ │ ├── backLinkFallback.js │ │ ├── htmlTemplating.feature │ │ ├── repeatField │ │ │ ├── confirmationTimeout.feature │ │ │ ├── samePageSummary.feature │ │ │ └── separatePageSummary.feature │ │ ├── redirect.feature │ │ ├── backLinkFallback.feature │ │ ├── files.feature │ │ ├── MiniSummaryPageController.feature │ │ ├── exit.js │ │ └── getConditionEvaluationContext.feature │ │ └── designer │ │ ├── accessibilityStatement.js │ │ ├── notifyOutput.feature │ │ ├── startPage.js │ │ ├── notifyOutput.js │ │ └── accessibilityStatement.feature └── package.json ├── queue-model ├── migrations │ ├── 20230919102214_use_db_text │ │ └── migration.sql │ ├── 20231107195736_add_allow_retry │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20231108100812_webhook_url_required │ │ └── migration.sql │ ├── 20230915145048_add_defaults │ │ └── migration.sql │ └── 20230913152003_init │ │ └── migration.sql ├── src │ └── index.ts ├── babel.config.json ├── tsconfig.json └── schema.prisma ├── docs ├── designer │ └── query-param-field.png ├── adr │ └── 0000-state-of-the-union.md ├── runner │ ├── mini-summary-page-controller.md │ └── arkit.json └── model │ └── arkit.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature_request.md └── dependabot.yml ├── .prettierignore ├── .dockerignore ├── .yarnrc.yml ├── babel.config.json ├── GitVersion.yml ├── lighthouserc.js └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /designer/.pa11yci.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designer/snowpack.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runner/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start 2 | -------------------------------------------------------------------------------- /runner/src/server/schemas/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 Chrome versions 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docker-compose.yml 2 | Dockerfile -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/Section.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designer/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /designer/client/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./i18n"; 2 | -------------------------------------------------------------------------------- /runner/bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build:css -------------------------------------------------------------------------------- /designer/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | unit-test.html 3 | coverage 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designer/client/data/section/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./addSection"; 2 | -------------------------------------------------------------------------------- /designer/client/components/Page/index.ts: -------------------------------------------------------------------------------- 1 | export { Page } from "./Page"; 2 | -------------------------------------------------------------------------------- /designer/__mocks__/imageMock.js: -------------------------------------------------------------------------------- 1 | module.exports = "pretend/path/to/image.png"; 2 | -------------------------------------------------------------------------------- /designer/client/components/Flyout/index.ts: -------------------------------------------------------------------------------- 1 | export { Flyout } from "./Flyout"; 2 | -------------------------------------------------------------------------------- /model/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export { Schema, componentSchema } from "./schema"; 2 | -------------------------------------------------------------------------------- /submitter/config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "test", 3 | "isTest": true 4 | } 5 | -------------------------------------------------------------------------------- /submitter/src/config.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | export default config; 3 | -------------------------------------------------------------------------------- /designer/client/components/BackLink/index.ts: -------------------------------------------------------------------------------- 1 | export { BackLink } from "./BackLink"; 2 | -------------------------------------------------------------------------------- /designer/client/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Menu } from "./Menu"; 2 | -------------------------------------------------------------------------------- /model/src/form/index.ts: -------------------------------------------------------------------------------- 1 | export { FormConfiguration } from "./form-configuration"; 2 | -------------------------------------------------------------------------------- /model/src/migration/index.ts: -------------------------------------------------------------------------------- 1 | export { whichMigrations } from "./whichMigrations"; 2 | -------------------------------------------------------------------------------- /model/src/migration/types.ts: -------------------------------------------------------------------------------- 1 | export type MigrationScript = (data: Object) => Object; 2 | -------------------------------------------------------------------------------- /designer/client/components/CssClasses/index.ts: -------------------------------------------------------------------------------- 1 | export { CssClasses } from "./CssClasses"; 2 | -------------------------------------------------------------------------------- /designer/client/components/FormDetails/index.ts: -------------------------------------------------------------------------------- 1 | export { FormDetails } from "./FormDetails"; 2 | -------------------------------------------------------------------------------- /designer/client/components/PageLinkage/index.ts: -------------------------------------------------------------------------------- 1 | export { PageLinkage } from "./PageLinkage"; 2 | -------------------------------------------------------------------------------- /designer/client/data/list/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findList"; 2 | export * from "./addList"; 3 | -------------------------------------------------------------------------------- /designer/client/hooks/list/useListItem/index.tsx: -------------------------------------------------------------------------------- 1 | export { useListItem } from "./useListItem"; 2 | -------------------------------------------------------------------------------- /designer/client/pages/ErrorPages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SaveError } from "./SaveError"; 2 | -------------------------------------------------------------------------------- /designer/client/components/Autocomplete/index.ts: -------------------------------------------------------------------------------- 1 | export { Autocomplete } from "./Autocomplete"; 2 | -------------------------------------------------------------------------------- /designer/client/components/ErrorMessage/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorMessage } from "./ErrorMessage"; 2 | -------------------------------------------------------------------------------- /designer/client/components/Visualisation/index.ts: -------------------------------------------------------------------------------- 1 | export { Visualisation } from "./Visualisation"; 2 | -------------------------------------------------------------------------------- /designer/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")({})], 3 | }; 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/constants.ts: -------------------------------------------------------------------------------- 1 | export const optionalText = " (optional)"; 2 | -------------------------------------------------------------------------------- /designer/client/components/ComponentCreate/index.ts: -------------------------------------------------------------------------------- 1 | export { ComponentCreate } from "./ComponentCreate"; 2 | -------------------------------------------------------------------------------- /designer/client/components/RenderInPortal/index.ts: -------------------------------------------------------------------------------- 1 | export { RenderInPortal } from "./RenderInPortal"; 2 | -------------------------------------------------------------------------------- /designer/client/styles/colors.scss: -------------------------------------------------------------------------------- 1 | @import "../../../node_modules/govuk-frontend/govuk/helpers/colour"; 2 | -------------------------------------------------------------------------------- /designer/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "500", 3 | "watch": ["dist"], 4 | "legacyWatch": true 5 | } 6 | -------------------------------------------------------------------------------- /runner/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "500", 3 | "watch": ["dist"], 4 | "legacyWatch": true 5 | } 6 | -------------------------------------------------------------------------------- /submitter/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "500", 3 | "watch": ["dist"], 4 | "legacyWatch": true 5 | } 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/initialiseSession/index.ts: -------------------------------------------------------------------------------- 1 | export { initialiseSession } from "./initialiseSession"; 2 | -------------------------------------------------------------------------------- /runner/performance/dummy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/runner/performance/dummy.pdf -------------------------------------------------------------------------------- /designer/client/components/CustomValidationMessage/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomValidationMessage } from "./CustomValidationMessage"; 2 | -------------------------------------------------------------------------------- /designer/client/context/index.ts: -------------------------------------------------------------------------------- 1 | export { DataContext } from "./DataContext"; 2 | export { FlyoutContext } from "./FlyoutContext"; 3 | -------------------------------------------------------------------------------- /e2e/cypress/fixtures/passes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/e2e/cypress/fixtures/passes.png -------------------------------------------------------------------------------- /queue-model/migrations/20230919102214_use_db_text/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Submission` MODIFY `data` TEXT NULL; 3 | -------------------------------------------------------------------------------- /runner/public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/runner/public/static/favicon.ico -------------------------------------------------------------------------------- /runner/src/client/sass/_govuk.scss: -------------------------------------------------------------------------------- 1 | $govuk-global-styles: true; 2 | @import "./../../../node_modules/govuk-frontend/dist/govuk/all"; 3 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/__tests__/summaryViewModel.jest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO: add tests for SummaryViewModel 3 | */ 4 | -------------------------------------------------------------------------------- /designer/client/components/ComponentCreate/ComponentCreate.scss: -------------------------------------------------------------------------------- 1 | .component-create { 2 | .back-link { 3 | margin-top: 0; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/designer/query-param-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/docs/designer/query-param-field.png -------------------------------------------------------------------------------- /e2e/cypress/fixtures/fails-ocr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/e2e/cypress/fixtures/fails-ocr.png -------------------------------------------------------------------------------- /runner/test/cases/server/dummy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XGovFormBuilder/digital-form-builder/HEAD/runner/test/cases/server/dummy.pdf -------------------------------------------------------------------------------- /designer/client/data/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./addComponent"; 2 | export * from "./updateComponent"; 3 | export * from "./inputs"; 4 | -------------------------------------------------------------------------------- /runner/bin/build-css: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node-sass --output-style=compressed \ 4 | --output=public/build/stylesheets \ 5 | src/client/sass 6 | -------------------------------------------------------------------------------- /runner/config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "isTest": true, 3 | "previewMode": true, 4 | "enforceCsrf": false, 5 | "env": "development" 6 | } 7 | -------------------------------------------------------------------------------- /submitter/src/submission/services/index.ts: -------------------------------------------------------------------------------- 1 | export { QueueService } from "./queueService"; 2 | export { WebhookService } from "./webhookService"; 3 | -------------------------------------------------------------------------------- /runner/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | src/server/public/build 3 | report.html 4 | report.json 5 | unit-test.html 6 | .env 7 | /dist/ 8 | /public/build/ 9 | -------------------------------------------------------------------------------- /runner/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "production", 3 | "logPrettyPrint": false, 4 | "enforceCsrf": true, 5 | "previewMode": false 6 | } 7 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/index.ts: -------------------------------------------------------------------------------- 1 | export { FormModel } from "./FormModel"; 2 | export { SummaryViewModel } from "./SummaryViewModel"; 3 | -------------------------------------------------------------------------------- /designer/server/views/split.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/html.html: -------------------------------------------------------------------------------- 1 | {% macro Html(component) %} 2 | {{ component.model.content | safe }} 3 | {% endmacro %} 4 | -------------------------------------------------------------------------------- /runner/src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as publicRoutes } from "./public"; 2 | export { default as healthCheckRoute } from "./health-check"; 3 | -------------------------------------------------------------------------------- /runner/src/server/services/upload/index.ts: -------------------------------------------------------------------------------- 1 | export { UploadService } from "./uploadService"; 2 | export { MockUploadService } from "./mockUploadService"; 3 | -------------------------------------------------------------------------------- /designer/jest-server-setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | 3 | beforeEach(() => { 4 | jest.resetAllMocks(); 5 | expect.hasAssertions(); 6 | }); 7 | -------------------------------------------------------------------------------- /runner/public/static/upload-dialog.js: -------------------------------------------------------------------------------- 1 | $("input[type='file']").on('change', function () { 2 | $(this).parent().parent().find('.upload-dialog').show(); 3 | }) 4 | -------------------------------------------------------------------------------- /designer/client/components/FormDetails/FormDetails.scss: -------------------------------------------------------------------------------- 1 | .form-details__feedback { 2 | & .govuk-radios__label { 3 | text-transform: capitalize; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /queue-model/migrations/20231107195736_add_allow_retry/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Submission` ADD COLUMN `allow_retry` BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /queue-model/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /runner/src/server/plugins/pulse.ts: -------------------------------------------------------------------------------- 1 | import pulse from "hapi-pulse"; 2 | 3 | export default { 4 | plugin: pulse, 5 | options: { 6 | timeout: 800, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /runner/bin/run/check/index.js: -------------------------------------------------------------------------------- 1 | const { check } = require("./check"); 2 | //NOTE:- the directory looks like this because of how sinon handles stubbing.. apologies. 3 | check(); 4 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_reload.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I reload", () => { 4 | cy.reload(); 5 | }); -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/para.html: -------------------------------------------------------------------------------- 1 | {% macro Para(component) %} 2 |

{{ component.model.content | safe }}

3 | {% endmacro %} 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/partials/conditional-components.html: -------------------------------------------------------------------------------- 1 | {% from "./components.html" import componentList with context %} 2 | 3 | {{ componentList(components) }} 4 | -------------------------------------------------------------------------------- /designer/client/data/condition/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./removeCondition"; 2 | export * from "./updateCondition"; 3 | export * from "./addCondition"; 4 | export * from "./hasConditions"; 5 | -------------------------------------------------------------------------------- /model/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ComponentTypes } from "./component-types"; 2 | export { ConditionalComponentTypes } from "./conditional-component-types"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /runner/bin/run/check/util.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | FORM_PATH: path.join(process.cwd(), "src", "server", "forms"), 5 | CURRENT_SCHEMA_VERSION: 2, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: gitter 4 | url: https://gitter.im/XGovFormBuilder/Public 5 | about: Please ask and answer questions here 6 | -------------------------------------------------------------------------------- /designer/client/data/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./allPathsLeadingTo"; 2 | export * from "./addLink"; 3 | export * from "./updateLink"; 4 | export * from "./findPage"; 5 | export * from "./updateLinksTo"; 6 | -------------------------------------------------------------------------------- /queue-model/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | export { PrismaClient, Prisma, Submission } from "@prisma/client"; 4 | export const SCHEMA_LOCATION = path.resolve(__dirname, "schema.prisma"); 5 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/datefield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro DateField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/numberfield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro NumberField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/timefield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro TimeField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /designer/client/context/FlyoutContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const FlyoutContext = createContext({ 4 | count: 0, 5 | increment: () => {}, 6 | decrement: () => {}, 7 | }); 8 | -------------------------------------------------------------------------------- /runner/src/server/config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from "./utils/configSchema"; 2 | import { default as nodeConfig } from "config"; 3 | 4 | const config = buildConfig(nodeConfig); 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/datetimefield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro DateTimeField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/textfield.html: -------------------------------------------------------------------------------- 1 | {% from "input/macro.njk" import govukInput %} 2 | 3 | {% macro TextField(component) %} 4 | {{ govukInput(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/websitefield.html: -------------------------------------------------------------------------------- 1 | {% from "input/macro.njk" import govukInput %} 2 | 3 | {% macro WebsiteField(component) %} 4 | {{ govukInput(component.model) }} 5 | {% endmacro %} -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/yesnofield.html: -------------------------------------------------------------------------------- 1 | {% from "./radiosfield.html" import RadiosField %} 2 | 3 | {% macro YesNoField(component) %} 4 | {{ RadiosField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - jenbutongit 10 | -------------------------------------------------------------------------------- /designer/client/data/condition/hasConditions.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | 3 | export function hasConditions(conditions: any[]): boolean { 4 | return conditions.length > 0; 5 | } 6 | -------------------------------------------------------------------------------- /designer/client/pages/LandingPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NewConfig } from "./NewConfig"; 2 | export { default as LandingChoice } from "./Choice"; 3 | export { default as ChooseExisting } from "./ChooseExisting"; 4 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_go_back.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I go back", () => { 4 | cy.findByRole("link", { name: "Back" }).click(); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/feedback/index.ts: -------------------------------------------------------------------------------- 1 | export { RelativeUrl } from "./RelativeUrl"; 2 | export { FeedbackContextInfo } from "./FeedbackContextInfo"; 3 | export { decodeFeedbackContextInfo } from "./helpers"; 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/radiosfield.html: -------------------------------------------------------------------------------- 1 | {% from "radios/macro.njk" import govukRadios %} 2 | 3 | {% macro RadiosField(component) %} 4 | {{ govukRadios(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/emailaddressfield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro EmailAddressField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/selectfield.html: -------------------------------------------------------------------------------- 1 | {% from "select/macro.njk" import govukSelect %} 2 | 3 | {% macro SelectField(component) %} 4 | {{ govukSelect(component.model) }} 5 | {% endmacro %} 6 | 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore 2 | node_modules 3 | **/node_modules/* 4 | test-coverage 5 | test-results 6 | dist 7 | npm-debug.log 8 | runner/performance 9 | public 10 | .yarn 11 | 12 | # Ignore all HTML files: 13 | *.html 14 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_choose_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I choose {string}", (string) => { 4 | cy.findByLabelText(string).check(); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_continue.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I continue", () => { 4 | cy.findByRole("button", { name: /continue/i }).click(); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_enter_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I enter {string}", (string) => { 4 | cy.findByRole("textbox").type(string); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_wait.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I wait {int} milliseconds", (milliseconds) => { 4 | cy.wait(Number(milliseconds)); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/telephonenumberfield.html: -------------------------------------------------------------------------------- 1 | {% from "./textfield.html" import TextField %} 2 | 3 | {% macro TelephoneNumberField(component) %} 4 | {{ TextField(component) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .idea 3 | */node_modules 4 | .env 5 | .gitignore 6 | */Dockerfile 7 | */README.md 8 | **/node_modules 9 | **/dist 10 | .yarn/build-state.yml 11 | .yarn/install-state.gz 12 | *.jest.* 13 | jest 14 | -------------------------------------------------------------------------------- /model/src/data-model/index.ts: -------------------------------------------------------------------------------- 1 | export { InputWrapper } from "./input-wrapper"; 2 | export { ConditionsWrapper, ConditionRawData } from "./conditions-wrapper"; 3 | export { Page, Item, Section, List, ConfirmationPage } from "./types"; 4 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/index.ts: -------------------------------------------------------------------------------- 1 | export { getFiles } from "./getFiles"; 2 | export { validateContentTypes } from "./validateContentTypes"; 3 | export { handleUpload } from "./handleUpload"; 4 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_am_redirected_to_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I am redirected to {string}", (url) => { 4 | cy.url().should("contain", url); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/checkboxesfield.html: -------------------------------------------------------------------------------- 1 | {% from "checkboxes/macro.njk" import govukCheckboxes %} 2 | 3 | {% macro CheckboxesField(component) %} 4 | {{ govukCheckboxes(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/datepartsfield.html: -------------------------------------------------------------------------------- 1 | {% from "date-input/macro.njk" import govukDateInput %} 2 | 3 | {% macro DatePartsField(component) %} 4 | {{ govukDateInput(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/monthyearfield.html: -------------------------------------------------------------------------------- 1 | {% from "date-input/macro.njk" import govukDateInput %} 2 | 3 | {% macro MonthYearField(component) %} 4 | {{ govukDateInput(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /designer/bin/symlink-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ENV_LOC=${1} 4 | LINK_TO=${2:-./.env} 5 | 6 | if [ -z "$ENV_LOC" ] 7 | then 8 | echo Where is .env? 9 | read -r ENV_LOC 10 | fi 11 | 12 | ln -s $ENV_LOC $LINK_TO 13 | -------------------------------------------------------------------------------- /designer/client/i18n/translations/cy.translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Create component": "Create component", 3 | "Edit page": "Golygu tudalen", 4 | "Persona": "Persona", 5 | "Preview": "Rhagolwg", 6 | "Preview page": "Tudalen rhagolwg" 7 | } 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_click_the_back_link.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click the back link", () => { 4 | cy.findByRole("link", { name: "Back" }).click(); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_select_the_option_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I select the option {string}", (string) => { 4 | cy.get("select").select(string); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/bin/symlink-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ENV_LOC=${1} 4 | LINK_TO=${2:-./.env} 5 | 6 | if [ -z "$ENV_LOC" ] 7 | then 8 | echo Where is .env? 9 | read -r ENV_LOC 10 | fi 11 | 12 | ln -s $ENV_LOC $LINK_TO 13 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/datetimepartsfield.html: -------------------------------------------------------------------------------- 1 | {% from "date-input/macro.njk" import govukDateInput %} 2 | 3 | {% macro DateTimePartsField(component) %} 4 | {{ govukDateInput(component.model) }} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/submission/index.ts: -------------------------------------------------------------------------------- 1 | export { EmailModel } from "./EmailModel"; 2 | export { FeesModel } from "./FeesModel"; 3 | export { NotifyModel } from "./NotifyModel"; 4 | export { WebhookModel } from "./WebhookModel"; 5 | -------------------------------------------------------------------------------- /submitter/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | testMatch: ["**/__tests__/**.test.ts"], 8 | }; 9 | -------------------------------------------------------------------------------- /submitter/src/submission/retention/errors.ts: -------------------------------------------------------------------------------- 1 | export const R_ERRORS = { 2 | CONFIG: `R001 - Could not set retention period`, 3 | DELETION_FAILED: `R002 - Could not delete records`, 4 | RUN_ERROR: `R003 - Could not run, caught exception`, 5 | }; 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_click_the_link_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click the link {string}", (string) => { 4 | cy.findByRole("link", { name: string }).click(); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_see_the_heading_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I see the heading {string}", (string) => { 4 | cy.findByRole("heading", { name: string }); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_submit_the_form.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I submit the form", () => { 4 | cy.findByRole("button", { name: /submit/i, exact: false }).click(); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/server/utils/generateCookiePassword.ts: -------------------------------------------------------------------------------- 1 | const generateCookiePassword = (): String => 2 | Array(32) 3 | .fill(0) 4 | .map(() => Math.random().toString(36).charAt(2)) 5 | .join(""); 6 | 7 | export default generateCookiePassword; 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_see_the_path_is_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I see the path is {string}", (string) => { 4 | cy.location("pathname").should("contain", string); 5 | }); 6 | -------------------------------------------------------------------------------- /queue-model/migrations/20231108100812_webhook_url_required/migration.sql: -------------------------------------------------------------------------------- 1 | -- UpdateColumn 2 | UPDATE `Submission` SET `webhook_url`='' WHERE `webhook_url` IS NULL; 3 | 4 | -- AlterTable 5 | ALTER TABLE `Submission` MODIFY `webhook_url` VARCHAR(191) NOT NULL; 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/insettext.html: -------------------------------------------------------------------------------- 1 | {% from "inset-text/macro.njk" import govukInsetText %} 2 | 3 | {% macro InsetText(component) %} 4 | {{ govukInsetText({ 5 | html: component.model.content 6 | }) }} 7 | {% endmacro %} 8 | -------------------------------------------------------------------------------- /designer/server/plugins/routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as newConfig from "./newConfig"; 2 | import * as api from "./api"; 3 | import * as app from "./app"; 4 | import { healthCheckRoute } from "./healthCheck"; 5 | 6 | export { newConfig, api, app, healthCheckRoute }; 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_dont_see_the_page_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I don't see the page {string}", (pageTitle) => { 4 | cy.findByText(pageTitle).should("not.exist"); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_select_string_for_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I select {string} for {string}", (option, label) => { 4 | cy.findByLabelText(label).select(option); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/client/sass/_upload-dialog.scss: -------------------------------------------------------------------------------- 1 | @import "./../../../node_modules/govuk-frontend/dist/govuk/helpers/colour"; 2 | 3 | .upload-dialog { 4 | display: none; 5 | } 6 | 7 | .govuk-border-green { 8 | border-color: govuk-colour("green"); 9 | } 10 | -------------------------------------------------------------------------------- /designer/client/pages/ErrorPages/ErrorPage.scss: -------------------------------------------------------------------------------- 1 | .error-summary { 2 | .back-link { 3 | margin-bottom: 40px; 4 | } 5 | } 6 | 7 | 8 | @media only screen and (min-height: 800px) { 9 | .error-summary { 10 | padding-bottom: 200px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_am_viewing_the_designer.js: -------------------------------------------------------------------------------- 1 | import { Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("I am viewing the designer at {string}", (path) => { 4 | cy.visit(`${Cypress.env("DESIGNER_URL")}${path}`); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/details.html: -------------------------------------------------------------------------------- 1 | {% from "details/macro.njk" import govukDetails %} 2 | 3 | {% macro Details(component) %} 4 | {# {{ getContext(component) | dump | safe }} #} 5 | {{ govukDetails(component.model)}} 6 | {% endmacro %} 7 | 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_have_read_the_disclaimer.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I have read the disclaimer", () => { 4 | cy.findByRole("checkbox").click(); 5 | cy.findByRole("button").click(); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_navigate_away_from_the_designer.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I navigate away from the designer workspace", () => { 4 | cy.window().invoke("history").invoke("back"); 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_am_viewing_the_runner_at_string.js: -------------------------------------------------------------------------------- 1 | import { Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("I am viewing the runner at {string}", (path = "/") => { 4 | cy.visit(`${Cypress.env.RUNNER_URL}${path}`); 5 | }); 6 | -------------------------------------------------------------------------------- /runner/src/index.ts: -------------------------------------------------------------------------------- 1 | import createServer from "./server"; 2 | 3 | createServer({}) 4 | .then((server) => server.start()) 5 | .then(() => process.send && process.send("online")) 6 | .catch((err) => { 7 | console.error(err); 8 | process.exit(1); 9 | }); 10 | -------------------------------------------------------------------------------- /designer/client/FeatureToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useFeatures } from "./hooks/featureToggling"; 2 | 3 | const FeatureToggle = ({ feature, children }) => { 4 | const features = useFeatures(); 5 | return features[feature] ? children : null; 6 | }; 7 | 8 | export default FeatureToggle; 9 | -------------------------------------------------------------------------------- /designer/client/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | export { ChevronRightIcon } from "./ChevronRightIcon"; 2 | export { EditIcon } from "./EditIcon"; 3 | export { MoveDownIcon } from "./MoveDownIcon"; 4 | export { MoveUpIcon } from "./MoveUpIcon"; 5 | export { SearchIcon } from "./SearchIcon"; 6 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/files.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I upload the file {string}", (filename) => { 4 | cy.get("input[type=file]").attachFile(filename); 5 | cy.findByRole("button", { name: /continue/i }).click(); 6 | }); 7 | -------------------------------------------------------------------------------- /queue-model/migrations/20230915145048_add_defaults/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Submission` MODIFY `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 3 | MODIFY `updated_at` DATETIME(3) NULL, 4 | MODIFY `complete` BOOLEAN NOT NULL DEFAULT false; 5 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/backLinkFallback.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("The back link href is {string}", (href) => { 4 | cy.findByRole("link", { name: "Back" }) 5 | .should("have.attr", "href") 6 | .and("eq", href); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_delete_the_page.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I delete the page", () => { 4 | cy.findByRole("button", { name: "Delete" }).click(); 5 | // cy.on("window:confirm", () => true); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_navigate_to_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I navigate to {string}", (url) => { 4 | cy.visit(`${Cypress.env("RUNNER_URL")}${url}`, { 5 | failOnStatusCode: false, 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /designer/server/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "./createServer"; 2 | 3 | createServer() 4 | .then((server) => server.start()) 5 | .then(() => process.send && process.send("online")) 6 | .catch((err) => { 7 | console.error(err); 8 | process.exit(1); 9 | }); 10 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_dont_see_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I don't see {string}", (string) => { 4 | cy.findByText(string, { ignore: ".autocomplete-wrapper option" }).should( 5 | "not.exist" 6 | ); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_enter_string_for_string.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 3 | 4 | When("I enter {string} for {string}", (answer, label) => { 5 | cy.findByLabelText(label).type(answer); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_expand_string_to_see_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I expand {string} to see {string}", (title, content) => { 4 | cy.get(".govuk-details__summary").click(); 5 | cy.findByText(content); 6 | }); 7 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | RelativeUrl, 3 | FeedbackContextInfo, 4 | decodeFeedbackContextInfo, 5 | } from "./feedback"; 6 | export { redirectTo, redirectUrl, nonRelativeRedirectUrl } from "./helpers"; 7 | export { configureEnginePlugin } from "./configureEnginePlugin"; 8 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/SummaryViewModel.detailsTransformationMap.ts: -------------------------------------------------------------------------------- 1 | import { SummaryDetailsTransformationMap } from "server/transforms/summaryDetails"; 2 | 3 | export const summaryDetailsTransformationMap: SummaryDetailsTransformationMap = require("../../../transforms/summaryDetails"); 4 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_select.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I select", (table) => { 4 | const values = table.raw()[0]; 5 | 6 | values.forEach((value) => { 7 | cy.findByLabelText(value).check(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /model/src/conditions/helpers.ts: -------------------------------------------------------------------------------- 1 | export function toPresentationString(condition) { 2 | return `${condition.coordinatorString()}${condition.conditionString()}`; 3 | } 4 | 5 | export function toExpression(condition) { 6 | return `${condition.coordinatorString()}${condition.conditionExpression()}`; 7 | } 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_see_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I see {string}", (string) => { 4 | cy.findByText(string, { 5 | exact: false, 6 | ignore: ".govuk-visually-hidden,title,.autocomplete-wrapper option", 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /runner/src/server/utils/url.ts: -------------------------------------------------------------------------------- 1 | export function isUrlSecure(matomoUrl: string) { 2 | try { 3 | const { protocol } = new URL(matomoUrl); 4 | 5 | if (protocol === "https:") { 6 | return true; 7 | } 8 | 9 | return false; 10 | } catch (error) { 11 | return false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_see_the_section_title_string_is_displayed_in_the_preview.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then( 4 | "I see the section title {string} is displayed in the preview", 5 | (string) => { 6 | cy.findByText(string); 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | 7 | logFilters: 8 | - code: "YN0013" 9 | level: discard 10 | 11 | yarnPath: .yarn/releases/yarn-3.2.2.cjs 12 | 13 | nmSelfReferences: false 14 | -------------------------------------------------------------------------------- /designer/server/plugins/logging.ts: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | import pino from "hapi-pino"; 3 | 4 | export default { 5 | plugin: pino, 6 | options: { 7 | prettyPrint: config.isDev, 8 | level: config.logLevel, 9 | logEvents: ["onPostStart", "onPostStop", "request-error"], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_choose_string_for_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I choose {string} for {string}", (option, label) => { 4 | cy.findByRole("group", { name: label }).within(() => { 5 | cy.findByLabelText(option).click(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_am_on_the_new_configuration_page.js: -------------------------------------------------------------------------------- 1 | import { Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("I am on the new configuration page", () => { 4 | cy.visit(`${Cypress.env("DESIGNER_URL")}/app/new`); 5 | cy.findByText("Enter a name for your form"); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_navigate_to_the_string_form.js: -------------------------------------------------------------------------------- 1 | import { Given, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("I navigate to the {string} form", (formName) => { 4 | cy.visit(`${Cypress.env("RUNNER_URL")}/${formName}`, { 5 | failOnStatusCode: false, 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/partials/components.html: -------------------------------------------------------------------------------- 1 | {% macro componentList(components) %} 2 | {% for component in components %} 3 | {% import "../components/" + component.type.toLowerCase() + ".html" as view with context %} 4 | {{ view[component.type](component) }} 5 | {% endfor %} 6 | {% endmacro %} 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_change_the_page_path_to_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I change the page path to {string}", (string) => { 4 | cy.findByLabelText("Path").clear().type(string); 5 | cy.findByRole("button", { name: "Save" }).click(); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_will_see_an_alert_warning_me_that_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I will see an alert warning me that {string}", (string) => { 4 | cy.on("window:confirm", (text) => { 5 | expect(text).to.contain(string); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_upload_a_file_that_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I upload a file that {string}", (result) => { 4 | cy.get("input[type=file]").attachFile(`${result}.png`); 5 | cy.findByRole("button", { name: /continue/i }).click(); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_change_the_page_title_to_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I change the page title to {string}", (string) => { 4 | cy.findByLabelText("Page title").clear().type(string); 5 | cy.findByRole("button", { name: "Save" }).click(); 6 | }); 7 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/partials/heading.html: -------------------------------------------------------------------------------- 1 | {% if sectionTitle and (sectionTitle !== pageTitle) %} 2 |

{{sectionTitle}}

3 | {% endif %} 4 | {% if showTitle %} 5 |

6 | {{pageTitle}} 7 |

8 | {% endif %} 9 | -------------------------------------------------------------------------------- /runner/src/server/services/payService.nanoid.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | import config from "server/config"; 3 | 4 | export const payReferenceLength = parseInt(config.payReferenceLength ?? 10); 5 | export const nanoid = customAlphabet( 6 | "1234567890ABCDEFGHIJKLMNPQRSTUVWXYZ-_", 7 | payReferenceLength 8 | ); 9 | -------------------------------------------------------------------------------- /designer/client/hooks/featureToggling.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { 3 | FeatureFlagContext, 4 | FeaturesInterface, 5 | } from "../context/FeatureFlagContext"; 6 | 7 | export const useFeatures = () => { 8 | const features: FeaturesInterface = useContext(FeatureFlagContext); 9 | return features; 10 | }; 11 | -------------------------------------------------------------------------------- /runner/src/server/forms/README.md: -------------------------------------------------------------------------------- 1 | # Pre-configured Forms 2 | 3 | This folder holds JSON files for pre-configured forms, which are forms that are automatically loaded by the runner. Please see `test.json` file as an example. 4 | All JSON files inside this folder are loaded and added to the engine plugin, please see `server/plugins/build/index.ts`. 5 | -------------------------------------------------------------------------------- /runner/src/server/plugins/initialiseSession/configurePlugin.ts: -------------------------------------------------------------------------------- 1 | import { initialiseSession } from "server/plugins/initialiseSession/initialiseSession"; 2 | 3 | export function configureInitialiseSessionPlugin(options: { 4 | safelist: string[]; 5 | }) { 6 | return { 7 | plugin: initialiseSession, 8 | options, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /designer/client/conditions/inline-condition-helpers.js: -------------------------------------------------------------------------------- 1 | import randomId from "../randomId"; 2 | 3 | export const tryParseInt = (val) => { 4 | let parsed = parseInt(val, 10); 5 | return isNaN(parsed) ? undefined : parsed; 6 | }; 7 | 8 | export const isInt = (val) => { 9 | const int = parseInt(val, 10); 10 | return !isNaN(int); 11 | }; 12 | -------------------------------------------------------------------------------- /designer/client/reducers/component/index.ts: -------------------------------------------------------------------------------- 1 | export { metaReducer } from "./componentReducer.meta"; 2 | export { optionsReducer } from "./componentReducer.options"; 3 | export { fieldsReducer } from "./componentReducer.fields"; 4 | export { schemaReducer } from "./componentReducer.schema"; 5 | export { ComponentContextProvider } from "./componentReducer"; 6 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/designer/accessibilityStatement.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I select {string} in the footer", (link) => { 4 | cy.get("footer").within(($footer) => { 5 | cy.findByRole("link", { name: link }) 6 | .invoke("removeAttr", "target") 7 | .click(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /submitter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/typescript", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "16" 9 | } 10 | } 11 | ] 12 | ], 13 | "exclude": ["node_modules/**"], 14 | "plugins": [ 15 | "@babel/plugin-transform-runtime" 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /designer/LICENSE: -------------------------------------------------------------------------------- 1 | The Open Government Licence (OGL) Version 3 2 | 3 | Crown copyright (c) 2018 4 | 5 | This source code is licensed under the Open Government Licence v3.0. To view this 6 | licence, visit www.nationalarchives.gov.uk/doc/open-government-licence/version/3 7 | or write to the Information Policy Team, The National Archives, Kew, Richmond, 8 | Surrey, TW9 4DU. -------------------------------------------------------------------------------- /designer/client/__tests__/page-edit.jest.tsx: -------------------------------------------------------------------------------- 1 | test.todo("Renders a form with the correct initial inputs"); 2 | test.todo( 3 | "updating the title changes the path if the path is the auto-generated one" 4 | ); 5 | test.todo("changing the section causes the new section to be selected"); 6 | test.todo("changing the controller causes the new controller to be selected"); 7 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/the_field_string_contains_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("the field {string} contains {string}", (label, value) => { 4 | cy.findByLabelText(label) 5 | .invoke("val") 6 | .then((val) => { 7 | expect(val).to.equal(value); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /model/LICENSE: -------------------------------------------------------------------------------- 1 | The Open Government Licence (OGL) Version 3 2 | 3 | Crown copyright (c) 2018 4 | 5 | This source code is licensed under the Open Government Licence v3.0. To view this 6 | licence, visit www.nationalarchives.gov.uk/doc/open-government-licence/version/3 7 | or write to the Information Policy Team, The National Archives, Kew, Richmond, 8 | Surrey, TW9 4DU. -------------------------------------------------------------------------------- /runner/LICENSE: -------------------------------------------------------------------------------- 1 | The Open Government Licence (OGL) Version 3 2 | 3 | Crown copyright (c) 2018 4 | 5 | This source code is licensed under the Open Government Licence v3.0. To view this 6 | licence, visit www.nationalarchives.gov.uk/doc/open-government-licence/version/3 7 | or write to the Information Policy Team, The National Archives, Kew, Richmond, 8 | Surrey, TW9 4DU. -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_see_the_string_page.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | const pagesMap = { 4 | upload: "Upload a file", 5 | imageQualityPlayback: "Check your image", 6 | summary: "Summary", 7 | }; 8 | Then("I see the {string} page", (page) => { 9 | cy.findByText(pagesMap[page]); 10 | }); 11 | -------------------------------------------------------------------------------- /queue-model/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": { 4 | "presets": [ 5 | "@babel/typescript", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "targets": { 10 | "node": "16" 11 | } 12 | } 13 | ] 14 | ], 15 | "sourceMaps": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /runner/bin/run/check/getJsonFiles.js: -------------------------------------------------------------------------------- 1 | const { FORM_PATH } = require("./util"); 2 | const path = require("path"); 3 | const fs = require("fs").promises; 4 | 5 | async function getJsonFiles() { 6 | return (await fs.readdir(FORM_PATH)).filter( 7 | (file) => path.extname(file) === ".json" 8 | ); 9 | } 10 | 11 | module.exports = { 12 | getJsonFiles, 13 | }; 14 | -------------------------------------------------------------------------------- /runner/config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "safelist": [ 3 | "61bca17e-fe74-40e0-9c15-a901ad120eca.mock.pstmn.io", 4 | "webho.ok", 5 | "localhost" 6 | ], 7 | "isTest": true, 8 | "previewMode": true, 9 | "enforceCsrf": false, 10 | "initialisedSessionKey": "predictable-key", 11 | "env": "test", 12 | "documentUploadApiUrl": "http://localhost:9000" 13 | } 14 | -------------------------------------------------------------------------------- /designer/server/views/includes/home-office-header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_see_the_error_string_for_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I see the error {string} for {string}", (error, fieldName) => { 4 | cy.findByText("There is a problem"); 5 | cy.findByRole("link", { name: error }); 6 | cy.findByRole("group", { description: `Error: ${error}` }); 7 | }); 8 | -------------------------------------------------------------------------------- /queue-model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "baseUrl": "./src", 4 | "compilerOptions": { 5 | "outDir": "dist/module", 6 | "rootDir": "src", 7 | "composite": true, 8 | "declaration": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./src"], 12 | "exclude": ["../node_modules", "node_modules", "./src/prisma"] 13 | } 14 | -------------------------------------------------------------------------------- /designer/client/context/DataContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { FormDefinition } from "@xgovformbuilder/model"; 3 | 4 | export const DataContext = createContext<{ 5 | data: FormDefinition; 6 | save: (toUpdate: FormDefinition) => Promise; 7 | }>({ 8 | data: {} as FormDefinition, 9 | save: async (_data: FormDefinition) => false, 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_edit_page_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I edit the page {string}", (string) => { 4 | cy.findByText(string, { ignore: ".govuk-visually-hidden" }) 5 | .closest(".page") 6 | .within(() => { 7 | cy.findByRole("button", { name: "Edit page" }).click(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /model/src/components/conditional-component-types.ts: -------------------------------------------------------------------------------- 1 | import { ConditionalComponent } from "./types"; 2 | 3 | export const ConditionalComponentTypes: ConditionalComponent[] = [ 4 | { 5 | name: "TextField", 6 | title: "Text field", 7 | subType: "field", 8 | }, 9 | { 10 | name: "NumberField", 11 | title: "Number field", 12 | subType: "field", 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /runner/test/html-helper.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("code"); 2 | 3 | class HtmlHelper { 4 | constructor($) { 5 | this.$ = $; 6 | } 7 | 8 | assertTitle(value) { 9 | expect(this.$("title").text()).to.equal(value); 10 | } 11 | 12 | assertError(value) { 13 | expect(this.$("title").text()).to.equal(value); 14 | } 15 | } 16 | 17 | module.exports = HtmlHelper; 18 | -------------------------------------------------------------------------------- /designer/client/randomId.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | /** 4 | * Custom alphabet is required because a number of formats of ID are invalid property names 5 | * and expr-eval (condition logic) will fail to execute. 6 | */ 7 | const randomId = customAlphabet( 8 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 9 | 6 10 | ); 11 | 12 | export default randomId; 13 | -------------------------------------------------------------------------------- /designer/jest-setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import "./test/testServer"; 3 | import { initI18n } from "./client/i18n"; 4 | initI18n(); 5 | 6 | beforeEach(() => { 7 | jest.resetAllMocks(); 8 | expect.hasAssertions(); 9 | document.body.innerHTML = ` 10 |
11 |
12 |
13 |
14 | `; 15 | }); 16 | -------------------------------------------------------------------------------- /designer/client/__tests__/helpers/mocks.ts: -------------------------------------------------------------------------------- 1 | import { Data } from "@xgovformbuilder/model"; 2 | 3 | export function simplePageMock() { 4 | return { 5 | pages: [ 6 | { 7 | title: "First page", 8 | path: "/first-page", 9 | components: [], 10 | controller: "./pages/summary.js", 11 | section: "home", 12 | }, 13 | ], 14 | lists: [], 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /docs/adr/0000-state-of-the-union.md: -------------------------------------------------------------------------------- 1 | # Architecture as of 26-03-2021 2 | 3 | * Status: is strong 4 | * Deciders: N/A 5 | * Date: 26-03-2021 when the decision was last updated 6 | 7 | ## Context 8 | This is just an area where architectural decisions/docs made on, or before 26-03-2021 can be recorded. 9 | 10 | ###ERD 11 | ![](0000-ERD.svg) 12 | 13 | ###Class Diagram 14 | ![](0000-class-diagram.svg) 15 | 16 | 17 | -------------------------------------------------------------------------------- /runner/src/server/templates/additionalContexts.json: -------------------------------------------------------------------------------- 1 | { 2 | "example": { 3 | "Answer 1": { 4 | "additionalInfo": "

This content is based on answer 1

", 5 | "listItems": "
  • Item 1
  • Item 2
  • " 6 | }, 7 | "Answer 2": { 8 | "additionalInfo": "

    This content is based on answer 2

    ", 9 | "listItems": "
  • Item 3
  • Item 4
  • " 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /designer/client/components/Visualisation/Info.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | updatedAt?: string; 5 | downloadedAt?: string; 6 | }; 7 | 8 | export const Info = ({ updatedAt, downloadedAt }: Props) => ( 9 |
    10 |

    last downloaded at {downloadedAt}

    11 |

    last updated at {updatedAt}

    12 |
    13 | ); 14 | -------------------------------------------------------------------------------- /model/src/migration/__tests__/whichMigrations.jest.ts: -------------------------------------------------------------------------------- 1 | import { whichMigrations } from "./.."; 2 | import { migrate as V0_TO_V2 } from "./../migration.0-2"; 3 | import { migrate as V1_TO_V2 } from "./../migration.1-2"; 4 | 5 | test("whichMigration determines which migration scrips should be applied", () => { 6 | expect(whichMigrations(0)).toEqual(new Set([V0_TO_V2])); 7 | expect(whichMigrations(1)).toEqual(new Set([V1_TO_V2])); 8 | }); 9 | -------------------------------------------------------------------------------- /designer/client/data/list/addList.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, List } from "@xgovformbuilder/model"; 2 | 3 | export function addList(data: FormDefinition, list: List): FormDefinition { 4 | const index = data.lists.findIndex((l) => l.name === list.name); 5 | if (index > -1) { 6 | throw Error(`A list with the name ${list.name} already exists`); 7 | } 8 | return { 9 | ...data, 10 | lists: [...data.lists, list], 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /designer/client/data/page/addPage.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, Page } from "@xgovformbuilder/model"; 2 | 3 | export function addPage(data: FormDefinition, page: Page): FormDefinition { 4 | const index = data.pages.findIndex((pg) => pg.path === page.path); 5 | if (index > -1) { 6 | throw Error(`A page with the path ${page.path} already exists`); 7 | } 8 | return { 9 | ...data, 10 | pages: [...data.pages, page], 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "baseUrl": "./src", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true, 8 | "declaration": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./src"], 12 | "exclude": [ 13 | "../node_modules", 14 | "node_modules", 15 | "**/*.test.ts", 16 | "test-coverage", 17 | "test-results" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /designer/client/components/BackLink/BackLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent } from "react"; 2 | 3 | import "./BackLink.scss"; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | href?: string; 8 | onClick?: (event: MouseEvent) => void; 9 | }; 10 | 11 | export const BackLink = ({ children, ...otherProps }: Props) => ( 12 | 13 | {children} 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /designer/client/data/list/findList.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, List } from "@xgovformbuilder/model"; 2 | import { Found } from "../types"; 3 | 4 | export function findList( 5 | data: FormDefinition, 6 | name: List["name"] 7 | ): Found { 8 | const index = data.lists.findIndex((list) => list.name === name); 9 | if (index < 0) { 10 | throw Error(`No list found with the name ${name}`); 11 | } 12 | return [data.lists[index], index]; 13 | } 14 | -------------------------------------------------------------------------------- /submitter/config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "NODE_ENV", 3 | "port": "PORT", 4 | 5 | "logPrettyPrint": "LOG_PRETTY_PRINT", 6 | "logLevel": "LOG_LEVEL", 7 | 8 | "queueDatabaseUrl": "QUEUE_DATABASE_URL", 9 | "queueDatabaseUsername": "QUEUE_DATABASE_USERNAME", 10 | "queueDatabasePassword": "QUEUE_DATABASE_PASSWORD", 11 | "pollingInterval": "QUEUE_POLLING_INTERVAL", 12 | "retentionPeriod": "QUEUE_RETENTION_PERIOD_DAYS" 13 | } 14 | -------------------------------------------------------------------------------- /submitter/src/submission/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "./createServer"; 2 | import { setupDatabase } from "./setupDatabase"; 3 | 4 | async function initApp() { 5 | await setupDatabase(); 6 | 7 | const server = await createServer(); 8 | try { 9 | await server.start(); 10 | process?.send?.("online"); 11 | } catch (e) { 12 | throw e; 13 | } 14 | } 15 | 16 | initApp().catch((err) => { 17 | console.error(err.message); 18 | }); 19 | -------------------------------------------------------------------------------- /designer/client/components/BackLink/BackLink.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/colors.scss"; 2 | 3 | a.back-link.govuk-back-link { 4 | text-decoration: none; 5 | color: govuk-colour("blue"); 6 | 7 | &::before { 8 | color: govuk-colour("blue"); 9 | border-color: govuk-colour("blue"); 10 | 11 | &:hover { 12 | border-color: govuk-colour("dark-blue"); 13 | } 14 | } 15 | 16 | &:hover { 17 | color: govuk-colour("dark-blue"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /designer/client/data/page/allPathsLeadingTo.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, Page } from "@xgovformbuilder/model"; 2 | import dfs from "depth-first"; 3 | 4 | export function allPathsLeadingTo(data: FormDefinition, path: Page["path"]) { 5 | const edges = data.pages.flatMap((page) => { 6 | return (page.next ?? []).map((next): [string, string] => [ 7 | page.path, 8 | next.path, 9 | ]); 10 | }); 11 | return dfs(edges, path, { reverse: true }); 12 | } 13 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/multilinetextfield.html: -------------------------------------------------------------------------------- 1 | {% from "textarea/macro.njk" import govukTextarea %} 2 | {% from "character-count/macro.njk" import govukCharacterCount %} 3 | 4 | {% macro MultilineTextField(component) %} 5 | {% if component.model.isCharacterOrWordCount == true %} 6 | {{ govukCharacterCount(component.model) }} 7 | {% else %} 8 | {{ govukTextarea(component.model) }} 9 | {% endif %} 10 | {% endmacro %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/InsetText.ts: -------------------------------------------------------------------------------- 1 | import { ComponentBase } from "./ComponentBase"; 2 | import { ViewModel } from "./types"; 3 | import { FormData, FormSubmissionErrors } from "../types"; 4 | 5 | export class InsetText extends ComponentBase { 6 | getViewModel(formData: FormData, errors: FormSubmissionErrors): ViewModel { 7 | return { 8 | ...super.getViewModel(formData, errors), 9 | content: this.content, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /runner/src/server/views/help/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block pageTitle %} 4 | Privacy Notice – {{ name if name else serviceName }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |
    11 |

    How we use your data (Privacy Notice)

    12 |
    13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /designer/client/components/Icons/ChevronRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ChevronRightIcon = (props: {}) => ( 4 | 16 | ); 17 | -------------------------------------------------------------------------------- /designer/client/components/Menu/useTabs.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useState } from "react"; 2 | 3 | export enum Tabs { 4 | model, 5 | json, 6 | summary, 7 | } 8 | 9 | export function useTabs() { 10 | const [selectedTab, setSelectedTab] = useState(Tabs.model); 11 | function handleTabChange(event: MouseEvent, tab: Tabs) { 12 | event.preventDefault(); 13 | setSelectedTab(tab); 14 | } 15 | 16 | return { selectedTab, handleTabChange }; 17 | } 18 | -------------------------------------------------------------------------------- /model/src/data-model/__tests__/isMultipleApiKey.test.ts: -------------------------------------------------------------------------------- 1 | import { isMultipleApiKey } from "../types"; 2 | 3 | test("isMultipleApiKey correctly typeguards", () => { 4 | expect(isMultipleApiKey("abc")).toBeFalsy(); 5 | expect(isMultipleApiKey({ test: "test_key" })).toBeTruthy(); 6 | expect(isMultipleApiKey({ production: "production_key" })).toBeTruthy(); 7 | expect( 8 | isMultipleApiKey({ test: "test_key", production: "production_key" }) 9 | ).toBeTruthy(); 10 | }); 11 | -------------------------------------------------------------------------------- /runner/public/static/object-from-entries-polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Object.fromEntries) { 2 | Object.fromEntries = function fromEntries(entries) { 3 | if (!entries) { 4 | throw new Error('Object.fromEntries() requires a single iterable argument'); 5 | } 6 | 7 | var obj = {}; 8 | 9 | for (var i = 0; i < entries.length; i++) { 10 | obj[entries[i][0]] = entries[i][1]; 11 | } 12 | 13 | return obj; 14 | } 15 | } -------------------------------------------------------------------------------- /designer/client/api/toggleApi.ts: -------------------------------------------------------------------------------- 1 | import logger from "../plugins/logger"; 2 | export class FeatureToggleApi { 3 | async fetch() { 4 | try { 5 | const response = await window.fetch("/feature-toggles"); 6 | if (response.status == 200) { 7 | return response.json(); 8 | } else { 9 | return []; 10 | } 11 | } catch (e) { 12 | logger.error("toggleApi", e); 13 | return []; 14 | } 15 | } 16 | } 17 | 18 | export default FeatureToggleApi; 19 | -------------------------------------------------------------------------------- /designer/client/components/Icons/MoveUpIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function MoveUpIcon() { 4 | return ( 5 | 6 | 16 | Move up 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /designer/client/__tests__/helpers.jest.tsx: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "../helpers"; 2 | 3 | test("isEmpty returns the correct value", () => { 4 | // @ts-ignore 5 | expect(isEmpty(1)).toBeFalsy(); 6 | // @ts-ignore 7 | expect(isEmpty(0)).toBeFalsy(); 8 | // @ts-ignore 9 | expect(isEmpty(-0)).toBeFalsy(); 10 | expect(isEmpty("boop")).toBeFalsy(); 11 | 12 | expect(isEmpty("")).toBeTruthy(); 13 | expect(isEmpty(``)).toBeTruthy(); 14 | expect(isEmpty(undefined)).toBeTruthy(); 15 | }); 16 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/exit/prehandlers/getBacklink.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | 3 | /** 4 | * This must be used **after** `getState`. Retrieves the user's last known page 5 | * for this form. 6 | */ 7 | export async function getBacklink( 8 | request: HapiRequest, 9 | _h: HapiResponseToolkit 10 | ) { 11 | const state = request.pre.state; 12 | const { progress } = state; 13 | return progress?.at?.(-1) ?? null; 14 | } 15 | -------------------------------------------------------------------------------- /designer/client/components/Icons/MoveDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function MoveDownIcon() { 4 | return ( 5 | 6 | 16 | Move down 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /designer/client/data/page/findPage.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, Page } from "@xgovformbuilder/model"; 2 | import { Found, Path } from ".."; 3 | 4 | /** 5 | * @returns returns a tuple of [Page, number] 6 | */ 7 | export function findPage(data: FormDefinition, path: Path): Found { 8 | const index = data.pages.findIndex((page) => page?.path === path); 9 | if (index < 0) { 10 | throw Error(`no page found with the path ${path}`); 11 | } 12 | return [{ ...data.pages[index] }, index]; 13 | } 14 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/feedback/helpers.ts: -------------------------------------------------------------------------------- 1 | import atob from "atob"; 2 | import { FeedbackContextInfo } from "./FeedbackContextInfo"; 3 | 4 | export function decodeFeedbackContextInfo( 5 | encoded: string | Buffer | undefined | null 6 | ): FeedbackContextInfo | void { 7 | if (encoded) { 8 | const decoded = JSON.parse(atob(encoded)); 9 | 10 | return new FeedbackContextInfo( 11 | decoded.formTitle, 12 | decoded.pageTitle, 13 | decoded.url 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/exit/prehandlers/getForm.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | import Boom from "boom"; 3 | 4 | /** 5 | * Gets the FormModel based on the URL parameter `/{id}`. 6 | */ 7 | export function getForm(request: HapiRequest, _h: HapiResponseToolkit) { 8 | const id = request.params?.id; 9 | const form = request.server.app.forms?.[id]; 10 | if (!form) { 11 | throw Boom.notFound(); 12 | } 13 | return form; 14 | } 15 | -------------------------------------------------------------------------------- /designer/client/data/condition/addCondition.ts: -------------------------------------------------------------------------------- 1 | import { ConditionRawData, FormDefinition } from "@xgovformbuilder/model"; 2 | 3 | export function addCondition( 4 | data: FormDefinition, 5 | condition: ConditionRawData 6 | ): FormDefinition { 7 | if (data.conditions.find((c) => condition.name === c.name)) { 8 | throw Error(`A condition with the name ${condition.name} already exists`); 9 | } 10 | return { 11 | ...data, 12 | conditions: [...data.conditions, condition], 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /docs/runner/mini-summary-page-controller.md: -------------------------------------------------------------------------------- 1 | # MiniSummaryPageController 2 | 3 | Below is an example of a page utilising this controller. It will display a summary of the form data in the section "YourDetails": 4 | 5 | ```json5 6 | { 7 | "title": "Check these details are correct before continuing", 8 | "path": "/check-your-details", 9 | "components": [], 10 | "next": [{ "path": "/next-page" }], 11 | "controller": "MiniSummaryPageController", 12 | "section": "YourDetails" 13 | } 14 | ``` -------------------------------------------------------------------------------- /submitter/src/__mocks__/prismaClient.ts: -------------------------------------------------------------------------------- 1 | import { mockDeep, mockReset, MockProxy } from "jest-mock-extended"; 2 | import { prisma as prismaClient } from "../prismaClient"; 3 | import { PrismaClient } from "@xgovformbuilder/queue-model"; 4 | 5 | jest.mock("../prismaClient", () => ({ 6 | __esModule: true, 7 | prisma: mockDeep(), 8 | })); 9 | 10 | beforeEach(() => { 11 | mockReset(prismaClient); 12 | }); 13 | 14 | export const prisma = (prismaClient as unknown) as MockProxy; 15 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/typescript"], 3 | "exclude": ["node_modules/**"], 4 | "plugins": [ 5 | "@babel/plugin-proposal-export-default-from", 6 | "@babel/plugin-transform-class-properties", 7 | "@babel/plugin-transform-private-property-in-object", 8 | "@babel/plugin-transform-private-methods", 9 | "@babel/plugin-transform-runtime", 10 | "@babel/plugin-syntax-jsx", 11 | "@babel/plugin-transform-logical-assignment-operators" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /designer/client/data/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This replaces the class data-model (deprecated) which we previously used to mutate {@link FormDefinition} before it is saved. 3 | * They were written FP style. When {@link FormDefinition} is passed in, it will be copied via destructuring `{...data}`, and the mutation applied. 4 | */ 5 | 6 | export * from "./types"; 7 | export * from "./component"; 8 | export * from "./list"; 9 | export * from "./page"; 10 | export * from "./section"; 11 | export * from "./condition"; 12 | -------------------------------------------------------------------------------- /designer/jest.server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/server"], 3 | displayName: "server", 4 | setupFilesAfterEnv: ["/jest-server-setup.js"], 5 | testMatch: ["/**/__tests__/*.jest.(ts|tsx)"], 6 | testPathIgnorePatterns: ["/test/"], 7 | coverageDirectory: "test-coverage/server/jest", 8 | coverageThreshold: { 9 | global: { 10 | branches: 50, 11 | functions: 48, 12 | lines: 56, 13 | statements: 55, 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /designer/server/lib/publish/index.ts: -------------------------------------------------------------------------------- 1 | import Wreck from "@hapi/wreck"; 2 | import config from "../../config"; 3 | 4 | export const publish = async function (id, configuration): Promise { 5 | try { 6 | return Wreck.post(`${config.publishUrl}/publish`, { 7 | payload: JSON.stringify({ id, configuration }), 8 | }); 9 | } catch (error) { 10 | throw new Error( 11 | `Error when publishing to endpoint ${config.publishUrl}/publish: message: ${error.message}` 12 | ); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_create_a_section_titled_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I create a section titled {string}", (string) => { 4 | cy.findByRole("link", { name: "Create section" }).click(); 5 | cy.findByLabelText("Section title").type(string); 6 | cy.findByTestId("flyout-1").within(() => { 7 | cy.findByRole("button", { name: "Save" }).click(); 8 | }); 9 | cy.findByRole("button", { name: "Save" }).click(); 10 | }); 11 | -------------------------------------------------------------------------------- /runner/src/server/transforms/summaryDetails/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { SummaryDetailsTransformationMap } from "server/transforms/summaryDetails/types"; 4 | export { SummaryDetailsTransformationMap }; 5 | 6 | /** 7 | * [View the docs for summary-details-transformations an explanation of how this feature works](docs/runner/summary-details-transforms.md) 8 | */ 9 | const summaryDetailsTransformations: SummaryDetailsTransformationMap = {}; 10 | 11 | module.exports = summaryDetailsTransformations; 12 | -------------------------------------------------------------------------------- /runner/src/server/views/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 |
    5 |
    6 |
    7 |
    8 |

    Sorry, there is a problem with the service

    9 |

    Contact your closest consulate.

    10 |
    11 |
    12 |
    13 |
    14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /runner/src/server/plugins/session.ts: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | import generateCookiePassword from "server/utils/generateCookiePassword"; 3 | 4 | export default { 5 | plugin: require("@hapi/yar"), 6 | options: { 7 | cache: { 8 | expiresIn: config.sessionTimeout, 9 | }, 10 | cookieOptions: { 11 | password: config.sessionCookiePassword || generateCookiePassword(), 12 | isSecure: !config.isDev, 13 | isHttpOnly: true, 14 | isSameSite: "Lax", 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /runner/src/server/services/index.ts: -------------------------------------------------------------------------------- 1 | export { UploadService, MockUploadService } from "./upload"; 2 | export { PayService } from "./payService"; 3 | export { NotifyService } from "./notifyService"; 4 | export { EmailService } from "./emailService"; 5 | export { CacheService, catboxProvider } from "./cacheService"; 6 | export { WebhookService } from "./webhookService"; 7 | export { StatusService } from "./statusService"; 8 | export { AddressService } from "./addressService"; 9 | export { ExitService } from "./ExitService"; 10 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/exit/prehandlers/parseExitEmailErrors.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | import { errorListFromValidationResult } from "./utils"; 3 | 4 | /** 5 | * Parses Joi errors that have been stored on `exitEmailError` 6 | */ 7 | export function parseExitEmailErrors( 8 | request: HapiRequest, 9 | _h: HapiResponseToolkit 10 | ) { 11 | const errors = request.yar.flash("exitEmailError"); 12 | return errorListFromValidationResult(errors); 13 | } 14 | -------------------------------------------------------------------------------- /designer/client/components/ErrorMessage/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import classNames from "classnames"; 3 | import { i18n } from "../../i18n"; 4 | 5 | interface Props { 6 | className?: string; 7 | } 8 | 9 | export const ErrorMessage: FC = ({ children, className, ...props }) => { 10 | return ( 11 | 12 | {i18n("error")} {children} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /model/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest", 5 | }, 6 | testMatch: ["/**/__tests__/*.(j|t)est.(ts|ts)"], 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 8 | coverageDirectory: "test-coverage", 9 | testPathIgnorePatterns: ["__tests__/helpers"], 10 | coverageThreshold: { 11 | global: { 12 | branches: 80, 13 | functions: 78, 14 | lines: 89, 15 | statements: 89, 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /runner/src/server/transforms/summaryDetails/types.ts: -------------------------------------------------------------------------------- 1 | import { FormModel } from "server/plugins/engine/models"; 2 | 3 | type TransformFunction =
    (value: Details) => Details; 4 | 5 | /** 6 | * This is a Record of FormModel basePath to transformation function, 7 | * e.g. 8 | * ``` 9 | * { 10 | * // test.json basePath will be "test" 11 | * "test": (value) => value, 12 | * } 13 | * ``` 14 | */ 15 | export type SummaryDetailsTransformationMap = Record< 16 | FormModel["basePath"], 17 | TransformFunction 18 | >; 19 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | assembly-informational-format: '{InformationalVersion}' 2 | mode: Mainline 3 | increment: Inherit 4 | continuous-delivery-fallback-tag: 'rc' 5 | major-version-bump-message: '(breaking|major):' 6 | minor-version-bump-message: '(feature|minor):' 7 | patch-version-bump-message: '(fix|patch):' 8 | commit-message-incrementing: Enabled 9 | 10 | branches: 11 | main: 12 | regex: ^main$ 13 | tag: 'rc' 14 | is-mainline: true 15 | ignore: 16 | sha: [1f302fae75c91c3a848281307696dfaa24529c22] 17 | commits-before: 2021-05-20 18 | -------------------------------------------------------------------------------- /designer/server/plugins/routes/healthCheck.ts: -------------------------------------------------------------------------------- 1 | import { ServerRoute } from "@hapi/hapi"; 2 | import config from "../../config"; 3 | 4 | export const healthCheckRoute: ServerRoute = { 5 | method: "GET", 6 | path: "/health-check", 7 | handler: function () { 8 | const date = new Date(); 9 | const uptime = process.uptime(); 10 | return { 11 | status: "OK", 12 | lastCommit: config.lastCommit, 13 | lastTag: config.lastTag, 14 | time: date.toUTCString(), 15 | uptime: uptime, 16 | }; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /queue-model/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = env("QUEUE_DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Submission { 11 | id Int @id @default(autoincrement()) 12 | webhook_url String @default("") 13 | created_at DateTime @default(now()) 14 | updated_at DateTime? 15 | data String? @db.Text 16 | error String? 17 | return_reference String? 18 | complete Boolean @default(false) 19 | retry_counter Int 20 | allow_retry Boolean @default(true) 21 | } -------------------------------------------------------------------------------- /designer/client/hooks/list/useListItem/types.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | 3 | export type ListItemHook = { 4 | handleTitleChange: (e) => void; 5 | handleConditionChange: (e) => void; 6 | handleValueChange: (e) => void; 7 | handleHintChange: (e) => void; 8 | prepareForDelete: (data: T, index?: number) => T; 9 | prepareForSubmit: (data: FormDefinition) => FormDefinition; 10 | validate: (i18n: any) => boolean; 11 | value: any; 12 | condition: any; 13 | title: string; 14 | hint: string; 15 | }; 16 | -------------------------------------------------------------------------------- /designer/client/components/ComponentCreate/__tests__/ComponentCreateList.jest.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { ComponentCreateList } from "../ComponentCreateList"; 4 | 5 | describe("ComponentCreateList", () => { 6 | test("should match snapshot", async () => { 7 | const onSelectComponent = jest.fn(); 8 | const { asFragment } = render( 9 | 10 | ); 11 | expect(asFragment()).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/RadiosField.ts: -------------------------------------------------------------------------------- 1 | import { SelectionControlField } from "./SelectionControlField"; 2 | 3 | /** 4 | * @description sorry about the empty class... 5 | * Exported Components must follow the naming convention implemented in @xgovformbuilder/model/components ComponentType. 6 | * In the Form JSON, components have a type property which is the name of the components, e.g. DateField. 7 | * Components are loaded in the ComponentsCollection constructor. 8 | */ 9 | export class RadiosField extends SelectionControlField {} 10 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/exit/prehandlers/getState.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | 3 | /** 4 | * Utility prehandler to get the user's state at the time the request was invoked. 5 | * (cacheService.getState will need to be called again if their state has been updated in later prehandlers or handlers) 6 | */ 7 | export async function getState(request: HapiRequest, _h: HapiResponseToolkit) { 8 | const { cacheService } = request.services([]); 9 | return cacheService.getState(request); 10 | } 11 | -------------------------------------------------------------------------------- /model/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Schema, componentSchema } from "./schema"; 2 | export { ConditionRawData, ConditionsWrapper } from "./data-model"; 3 | export { Logger } from "./utils/logger"; 4 | export { FormConfiguration } from "./form"; 5 | export { ComponentTypes, ConditionalComponentTypes } from "./components"; 6 | export * from "./components/types"; 7 | export * from "./conditions"; 8 | export * from "./utils/helpers"; 9 | export * from "./migration"; 10 | export * from "./data-model/types"; 11 | export { whichMigrations } from "./migration/whichMigrations"; 12 | -------------------------------------------------------------------------------- /designer/test/helpers/sub-component-assertions.js: -------------------------------------------------------------------------------- 1 | import { Input } from "@govuk-jsx/input"; 2 | import * as Code from "@hapi/code"; 3 | 4 | const { expect } = Code; 5 | 6 | export function assertInputControlValue({ wrapper, id, expectedValue }) { 7 | return assertInputControlProp({ wrapper, id, prop: "value", expectedValue }); 8 | } 9 | 10 | export function assertInputControlProp({ wrapper, id, prop, expectedValue }) { 11 | expect( 12 | wrapper 13 | .find(Input) 14 | .filter("#" + id) 15 | .prop(prop) 16 | ).to.equal(expectedValue); 17 | } 18 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/i_see_a_summary_list_with_the_values.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { findByText } from "@testing-library/dom"; 3 | Then("I see a summary list with the values", (table) => { 4 | const expectedRows = table.hashes(); 5 | 6 | const rows = cy.findAllByRole("term"); 7 | 8 | rows.each((row, i) => { 9 | const parent = row.parent()[0]; 10 | findByText(parent, expectedRows[i].title, { ignore: "span" }); 11 | findByText(parent, expectedRows[i].value); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /queue-model/migrations/20230913152003_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Submission` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `webhook_url` VARCHAR(191) NULL, 5 | `created_at` DATETIME(3) NOT NULL, 6 | `updated_at` DATETIME(3) NOT NULL, 7 | `data` VARCHAR(8192) NOT NULL, 8 | `error` VARCHAR(191) NULL, 9 | `return_reference` VARCHAR(191) NULL, 10 | `complete` BOOLEAN NOT NULL, 11 | `retry_counter` INTEGER NOT NULL, 12 | 13 | PRIMARY KEY (`id`) 14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 15 | -------------------------------------------------------------------------------- /runner/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/typescript", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "16" 9 | } 10 | } 11 | ] 12 | ], 13 | "exclude": ["node_modules/**"], 14 | "plugins": [ 15 | "@babel/plugin-transform-runtime", 16 | [ 17 | "module-name-mapper", 18 | { 19 | "moduleNameMapper": { 20 | "^src/(.*)": "/src/$1", 21 | "^server/(.*)": "/src/server/$1" 22 | } 23 | } 24 | ] 25 | ] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /designer/client/__mocks__/tabbable.js: -------------------------------------------------------------------------------- 1 | const lib = jest.requireActual("tabbable"); 2 | const tabbable = { 3 | ...lib, 4 | tabbable: (node, options) => 5 | lib.tabbable(node, { ...options, displayCheck: "none" }), 6 | focusable: (node, options) => 7 | lib.focusable(node, { ...options, displayCheck: "none" }), 8 | isFocusable: (node, options) => 9 | lib.isFocusable(node, { ...options, displayCheck: "none" }), 10 | isTabbable: (node, options) => 11 | lib.isTabbable(node, { ...options, displayCheck: "none" }), 12 | }; 13 | 14 | module.exports = tabbable; 15 | -------------------------------------------------------------------------------- /runner/src/server/views/help/accessibility-statement.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block pageTitle %} 4 | Accessibility Statement – {{ name if name else serviceName }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |
    11 |
    12 |

    Accessibility Statement

    13 |
    14 |
    15 |
    16 |
    17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /designer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ["plugin:react/recommended", "plugin:react-hooks/recommended"], 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | ecmaVersion: 12, 13 | sourceType: "module", 14 | }, 15 | plugins: ["react", "react-hooks"], 16 | rules: { 17 | "react/prop-types": 0, 18 | "react-hooks/rules-of-hooks": "warn", 19 | }, 20 | settings: { 21 | react: { 22 | version: "detect", 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /designer/client/data/condition/removeCondition.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | 3 | export function removeCondition(data: FormDefinition, name) { 4 | const pages = [...data.pages].map((page) => { 5 | return { 6 | ...page, 7 | next: 8 | page.next?.map((next) => 9 | next.condition === name ? { ...next, condition: undefined } : next 10 | ) ?? [], 11 | }; 12 | }); 13 | return { 14 | ...data, 15 | pages, 16 | conditions: data.conditions.filter((condition) => condition.name !== name), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/runner/the_form_string_exists.js: -------------------------------------------------------------------------------- 1 | import { Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("the form {string} exists", (formName) => { 4 | const url = `${Cypress.env("RUNNER_URL")}/publish`; 5 | 6 | cy.fixture(`${formName}.json`, "utf-8").then((json) => { 7 | const requestBody = { 8 | id: formName, 9 | configuration: json, 10 | }; 11 | cy.request("POST", url, requestBody); 12 | }); 13 | 14 | cy.visit(`${Cypress.env("RUNNER_URL")}/${formName}`, { 15 | failOnStatusCode: false, 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/Para.ts: -------------------------------------------------------------------------------- 1 | import { ComponentBase } from "./ComponentBase"; 2 | import { FormData, FormSubmissionErrors } from "../types"; 3 | 4 | export class Para extends ComponentBase { 5 | getViewModel(formData: FormData, errors: FormSubmissionErrors) { 6 | const options: any = this.options; 7 | const viewModel = { 8 | ...super.getViewModel(formData, errors), 9 | content: this.content, 10 | }; 11 | 12 | if (options.condition) { 13 | viewModel.condition = options.condition; 14 | } 15 | return viewModel; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /submitter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "outDir": "dist", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": false, 8 | "esModuleInterop": true, 9 | "paths": { 10 | "src/*": ["src/*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "lib": [ 14 | "dom", 15 | "ES2020.Promise", 16 | "ES2019.Object", 17 | "ES2019.Array" 18 | ], 19 | "skipLibCheck": true 20 | }, 21 | "include": [ 22 | "src", 23 | "package.json" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /designer/client/components/RenderInPortal/RenderInPortal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | export class RenderInPortal extends React.Component { 5 | wrapper = document.createElement("div"); 6 | portalRoot = document.getElementById("portal-root")!; 7 | 8 | componentDidMount() { 9 | this.portalRoot.appendChild(this.wrapper); 10 | } 11 | 12 | componentWillUnmount() { 13 | this.portalRoot.removeChild(this.wrapper); 14 | } 15 | 16 | render() { 17 | return ReactDOM.createPortal(this.props.children, this.wrapper); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /model/src/conditions/condition-value-registration.ts: -------------------------------------------------------------------------------- 1 | const conditionValueFactories = {}; 2 | 3 | export class Registration { 4 | type: string; 5 | 6 | constructor(type: string, factory: (obj: any) => Registration) { 7 | conditionValueFactories[type] = factory; 8 | this.type = type; 9 | } 10 | 11 | static register(type: string, factory: (obj: any) => Registration) { 12 | return new Registration(type, factory); 13 | } 14 | 15 | static conditionValueFrom(obj: { type: string; [prop: string]: any }) { 16 | return conditionValueFactories[obj.type](obj); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /designer/client/modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Modal(props) { 4 | if (!props.show) { 5 | return null; 6 | } 7 | 8 | return ( 9 | 24 | ); 25 | } 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /submitter/src/submission/plugins/logging.ts: -------------------------------------------------------------------------------- 1 | import pino from "hapi-pino"; 2 | import config from "../../config"; 3 | 4 | export const pluginLogging = { 5 | plugin: pino, 6 | options: { 7 | prettyPrint: false, 8 | level: config.logLevel, 9 | formatters: { 10 | level: (label) => { 11 | return { level: label }; 12 | }, 13 | }, 14 | debug: config.isDev, 15 | logRequestStart: config.isDev, 16 | logRequestComplete: config.isDev, 17 | redact: { 18 | paths: ["req.headers['x-forwarded-for']"], 19 | censor: "REDACTED", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /submitter/src/submission/plugins/retentionCron.ts: -------------------------------------------------------------------------------- 1 | import HapiCron from "hapi-cron"; 2 | import pino from "pino"; 3 | const logger = pino(); 4 | export const pluginRetentionCron = { 5 | plugin: HapiCron, 6 | options: { 7 | jobs: [ 8 | { 9 | name: "retention-cron", 10 | time: "*/1 * * * *", 11 | timezone: "Europe/London", 12 | request: { 13 | method: "GET", 14 | url: "/retention", 15 | }, 16 | onComplete: () => { 17 | logger.info("retention-cron complete"); 18 | }, 19 | }, 20 | ], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /model/src/form/form-configuration.ts: -------------------------------------------------------------------------------- 1 | export class FormConfiguration { 2 | Key: string; 3 | DisplayName: string; 4 | LastModified: string | undefined; 5 | feedbackForm: boolean | undefined; 6 | 7 | constructor( 8 | Key: string, 9 | DisplayName?: string, 10 | LastModified?: string, 11 | feedbackForm?: boolean 12 | ) { 13 | if (!Key) { 14 | throw Error("Form configuration must have a key"); 15 | } 16 | this.Key = Key; 17 | this.DisplayName = DisplayName || Key; 18 | this.LastModified = LastModified; 19 | this.feedbackForm = feedbackForm || false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /designer/client/hooks/__tests__/FeatureTogglingHook.jest.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFeatures } from "../featureToggling"; 3 | import sinon from "sinon"; 4 | 5 | describe("FeatureToggleHook", () => { 6 | it("should return feature context value", () => { 7 | const mockContextValue = { 8 | featureA: false, 9 | featureB: true, 10 | featureC: true, 11 | }; 12 | sinon.stub(React, "useContext").callsFake(function () { 13 | return mockContextValue; 14 | }); 15 | const result = useFeatures(); 16 | expect(result).toEqual(mockContextValue); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/feedback/FeedbackContextInfo.ts: -------------------------------------------------------------------------------- 1 | import btoa from "btoa"; 2 | import { RelativeUrl } from "./RelativeUrl"; 3 | 4 | export class FeedbackContextInfo { 5 | formTitle: string; 6 | pageTitle: string; 7 | url: string; 8 | 9 | constructor(formTitle, pageTitle, url) { 10 | this.formTitle = formTitle; 11 | this.pageTitle = pageTitle; 12 | // parse as a relative Url to ensure they're sensible values and prevent phishing 13 | this.url = url ? new RelativeUrl(url).toString() : url; 14 | } 15 | 16 | toString() { 17 | return btoa(JSON.stringify(this)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /runner/src/server/routes/health-check.ts: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | 3 | /** 4 | * A route which helps k8s determine whether a pod managed to start successfully. If a pod is still not running, k8s will try to restart it. 5 | */ 6 | export default { 7 | method: "GET", 8 | path: "/health-check", 9 | handler: function () { 10 | const date = new Date(); 11 | const uptime = process.uptime(); 12 | return { 13 | status: "OK", 14 | lastCommit: config.lastCommit, 15 | lastTag: config.lastTag, 16 | time: date.toUTCString(), 17 | uptime: uptime, 18 | }; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /designer/server/lib/persistence/blobPersistenceService.ts: -------------------------------------------------------------------------------- 1 | import { PersistenceService } from "./persistenceService"; 2 | 3 | export class BlobPersistenceService implements PersistenceService { 4 | logger: any; 5 | 6 | uploadConfiguration(_id: string, _configuration: string) { 7 | return Promise.resolve(undefined); 8 | } 9 | 10 | listAllConfigurations() { 11 | return Promise.resolve([]); 12 | } 13 | 14 | getConfiguration(_id: string) { 15 | return Promise.resolve(""); 16 | } 17 | copyConfiguration(_configurationId: string, _newName: string) { 18 | return Promise.resolve(""); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /designer/test/helpers/react-testing-library-utils.ts: -------------------------------------------------------------------------------- 1 | import { screen } from "@testing-library/react"; 2 | import userEvent from "@testing-library/user-event"; 3 | 4 | export const findInput = async (label) => 5 | (await screen.findByLabelText(label)) as HTMLInputElement; 6 | 7 | export const findInputValue = async (label) => { 8 | const input = (await screen.findByLabelText(label)) as HTMLInputElement; 9 | return input.value; 10 | }; 11 | 12 | export const typeIntoInput = async (label, value) => { 13 | const input = await findInput(label); 14 | userEvent.clear(input); 15 | userEvent.type(input, value); 16 | }; 17 | -------------------------------------------------------------------------------- /designer/client/data/section/addSection.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, Section } from "@xgovformbuilder/model"; 2 | 3 | /** 4 | * @param data - data from DataContext 5 | * @param section - the section to add 6 | * @throws Error - if a section already exists with the same name 7 | */ 8 | export function addSection( 9 | data: FormDefinition, 10 | section: Section 11 | ): FormDefinition { 12 | if (data.sections.find((s) => s.name === section.name)) { 13 | throw Error(`A section with the name ${section.name} already exists`); 14 | } 15 | return { 16 | ...data, 17 | sections: [...data.sections, section], 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/htmlTemplating.feature: -------------------------------------------------------------------------------- 1 | Feature: HTML templating in forms 2 | 3 | Scenario: Correct content should be shown for option one 4 | When the form "html-templating-example" exists 5 | And I choose "Answer 1" 6 | And I continue 7 | Then I see "This content is based on answer 1" 8 | And I see "Item 1" 9 | And I see "Item 2" 10 | 11 | Scenario: Correct content should be shown for option 2 12 | When the form "html-templating-example" exists 13 | And I choose "Answer 2" 14 | And I continue 15 | Then I see "This content is based on answer 2" 16 | And I see "Item 3" 17 | And I see "Item 4" -------------------------------------------------------------------------------- /lighthouserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ci: { 3 | collect: { 4 | url: [ 5 | "http://localhost:3000/app", 6 | "http://localhost:3000/app/new", 7 | "http://localhost:3000/app/choose-existing", 8 | "http://localhost:3000/app/designer/test-form-a", 9 | "http://localhost:3009/components/all-components", 10 | ], 11 | startServerCommand: "", 12 | settings: { 13 | //set which categories you want to run here. 14 | onlyCategories: ["accessibility"], 15 | }, 16 | }, 17 | assert: {}, 18 | upload: { 19 | target: "filesystem", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /model/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | server: any; 3 | name: string; 4 | 5 | constructor(server: any, name: string) { 6 | this.server = server; 7 | this.name = name; 8 | } 9 | 10 | error(message: string) { 11 | this.log("error", message); 12 | } 13 | 14 | warn(message: string) { 15 | this.log("warn", message); 16 | } 17 | 18 | info(message: string) { 19 | this.log("info", message); 20 | } 21 | 22 | debug(message: string) { 23 | this.log("debug", message); 24 | } 25 | 26 | log(level: string, message: string) { 27 | this.server.log([level, this.name], message); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request/discuss a feature 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/designer/notifyOutput.feature: -------------------------------------------------------------------------------- 1 | Feature: Notify output allows lists 2 | As a user 3 | I want to add a notify output 4 | So that I can send emails to customers using values from the form submission 5 | 6 | Background: 7 | Given the form "notifyOutput" exists 8 | When I am viewing the designer at "/app/designer/notifyOutput" 9 | Then The list "New list" should exist 10 | 11 | Scenario: Create GOVNotify output 12 | When I open Outputs 13 | * I choose Add output 14 | * I use the GOVUK notify output type 15 | * I add a personalisation 16 | Then "New list (List)" should appear in the Description dropdown 17 | -------------------------------------------------------------------------------- /runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": false, 8 | "esModuleInterop": true, 9 | "paths": { 10 | "src/*": ["src/*"], 11 | "server/*": ["src/server/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "lib": [ 15 | "dom", 16 | "ES2021.String", 17 | "ES2020.String", 18 | "ES2020.Promise", 19 | "ES2019.Object", 20 | "ES2019.Array" 21 | ], 22 | "skipLibCheck": true 23 | }, 24 | "include": ["src", "package.json"] 25 | } 26 | -------------------------------------------------------------------------------- /designer/client/data/component/addComponent.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDef, FormDefinition } from "@xgovformbuilder/model"; 2 | import { Path } from "../../reducers/data/types"; 3 | import { findPage } from "../page"; 4 | 5 | export function addComponent( 6 | data: FormDefinition, 7 | pagePath: Path, 8 | component: ComponentDef 9 | ): FormDefinition { 10 | const [page, index] = findPage(data, pagePath); 11 | 12 | const { components = [] } = page; 13 | const updatedPage = { ...page, components: [...components, component] }; 14 | return { 15 | ...data, 16 | pages: data.pages.map((page, i) => (index === i ? updatedPage : page)), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/Details.ts: -------------------------------------------------------------------------------- 1 | import { FormData, FormSubmissionErrors } from "../types"; 2 | import { ComponentBase } from "./ComponentBase"; 3 | 4 | export class Details extends ComponentBase { 5 | getViewModel(formData: FormData, errors: FormSubmissionErrors) { 6 | const { options } = this; 7 | 8 | const viewModel = { 9 | ...super.getViewModel(formData, errors), 10 | summaryHtml: this.title, 11 | html: this.content, 12 | }; 13 | 14 | if ("condition" in options && options.condition) { 15 | viewModel.condition = options.condition; 16 | } 17 | 18 | return viewModel; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /designer/client/data/page/updateLinksTo.ts: -------------------------------------------------------------------------------- 1 | import { Path } from ".."; 2 | import { FormDefinition, Page } from "@xgovformbuilder/model"; 3 | 4 | export function updateLinksTo( 5 | data: FormDefinition, 6 | oldPath: Path, 7 | newPath: Path 8 | ): FormDefinition { 9 | return { 10 | ...data, 11 | pages: data.pages.map( 12 | (page): Page => ({ 13 | ...page, 14 | path: page.path === oldPath ? newPath : page.path, 15 | next: 16 | page.next?.map((link) => ({ 17 | ...link, 18 | path: link.path === oldPath ? newPath : link.path, 19 | })) ?? [], 20 | }) 21 | ), 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /designer/client/outputs/webhook-edit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ValidationErrors } from "./types"; 3 | import { Input } from "@govuk-jsx/input"; 4 | 5 | type Props = { 6 | url: string; 7 | errors: ValidationErrors; 8 | }; 9 | 10 | const WebhookEdit = ({ url = "", errors }: Props) => ( 11 | 22 | ); 23 | 24 | export default WebhookEdit; 25 | -------------------------------------------------------------------------------- /designer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": ".", 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "composite": true, 7 | "declaration": true, 8 | "declarationDir": "dist/types", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "lib": ["DOM", "es2018.promise", "ES2019"] 12 | }, 13 | "include": [ 14 | "server", 15 | "client", 16 | "test", 17 | "package.json", 18 | "new-form.json", 19 | "client/i18n/translations/en.translation.json", 20 | "test/helpers/window-stubbing.js" 21 | ], 22 | "paths": { 23 | "*": ["node_modules/*", "../node_modules"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/repeatField/confirmationTimeout.feature: -------------------------------------------------------------------------------- 1 | Feature: Confirmation timeout 2 | 3 | Background: Given the form "confirmation-timeout" exists 4 | 5 | Scenario: Cannot see status page after waiting 10 seconds 6 | When I navigate to the "confirmation-timeout" form 7 | And I choose "Yes" 8 | And I continue 9 | Then I see a summary list with the values 10 | | title | value | 11 | | Filler question | Yes | 12 | When I submit the form 13 | Then I see "Application complete" 14 | When I wait 10000 milliseconds 15 | And I reload 16 | Then I don't see "Application complete" -------------------------------------------------------------------------------- /designer/client/components/Menu/useMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | type MenuItemHook = { 4 | isVisible: boolean; 5 | show: (e?: React.MouseEvent) => void; 6 | hide: (e?: React.MouseEvent) => void; 7 | }; 8 | 9 | export function useMenuItem(): MenuItemHook { 10 | const [isVisible, setIsVisible] = useState(false); 11 | 12 | function show(e) { 13 | e?.preventDefault(); 14 | setIsVisible(true); 15 | } 16 | 17 | function hide(e) { 18 | e?.preventDefault(); 19 | setIsVisible(false); 20 | } 21 | 22 | return { 23 | isVisible, 24 | show, 25 | hide, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /designer/client/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | 2 | /* MIXINS AND FUNCTIONS 3 | * 4 | * BEM NAMING CONVENTION 5 | * Elements are noted with __ (double underscore), for example .chapter__heading 6 | * Modifiers are noted with -- (double dash), for example .chapter--splash 7 | * Element mixin: @include e(heading) outputs &__heading 8 | * Modifier mixin: @include m(splash) outputs &--splash 9 | * WARNING:- you can't use this with parent selector! 10 | * 11 | */ 12 | 13 | //element selector 14 | @mixin e($element) { 15 | &__#{$element} { 16 | @content; 17 | } 18 | } 19 | 20 | //modifier selector 21 | @mixin m($modifier) { 22 | &--#{$modifier} { 23 | @content; 24 | } 25 | } -------------------------------------------------------------------------------- /runner/src/server/views/application-error.html: -------------------------------------------------------------------------------- 1 | {% from "error-summary/macro.njk" import govukErrorSummary %} 2 | 3 | {% extends 'layout.html' %} 4 | {% block content %} 5 |
    6 |
    7 | {% set tmpl = 'There was a problem with your application' %} 8 | 9 | {{ govukErrorSummary({ 10 | titleText: tmpl, 11 | errorList: [ 12 | { 13 | text: "There was an error processing your application" 14 | } 15 | ] 16 | }) }} 17 | 18 |

    Contact your closest consulate.

    19 | 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /designer/client/plugins/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | const logLevel = process.env.REACT_LOG_LEVEL; 3 | 4 | export default pino({ 5 | name: "designer", 6 | browser: { 7 | asObject: true, 8 | transmit: { 9 | level: logLevel, 10 | send: async function (_level, logEvent) { 11 | const newResponse = await window.fetch("/api/log", { 12 | method: "POST", 13 | body: JSON.stringify(logEvent), 14 | headers: { 15 | Accept: "application/json", 16 | "Content-Type": "application/json", 17 | }, 18 | }); 19 | return newResponse.json(); 20 | }, 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/designer/i_preview_the_page_string.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I preview the page {string}", (pageName) => { 4 | cy.findByText(pageName, { ignore: ".govuk-visually-hidden" }) 5 | .closest(".page") 6 | .within(() => { 7 | cy.get(`a[title="Preview page"]`) 8 | .invoke("attr", "href") 9 | .then(($val) => { 10 | cy.origin( 11 | `http://localhost:3009`, 12 | { args: { pageName: $val } }, 13 | ({ pageName }) => { 14 | cy.visit(pageName); 15 | } 16 | ); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /runner/src/client/sass/_hmpo.scss: -------------------------------------------------------------------------------- 1 | @import "./../node_modules/hmpo-components/assets/stylesheets/mixins"; 2 | @import "./../node_modules/hmpo-components/components/hmpo-circle-step/style"; 3 | @import "./../node_modules/hmpo-components/components/hmpo-flash-card/style"; 4 | 5 | /* from "hmpo-components/assets/stylesheets/_typeography.scss" 6 | * fixes typography for circle steps. The whole file messes up confirmation/panel. 7 | **/ 8 | main ol, 9 | main ul { 10 | @extend .govuk-list; 11 | } 12 | 13 | .flash-card { 14 | h2 { 15 | margin-bottom: 0; 16 | } 17 | h2, p { 18 | margin-top: 0; 19 | } 20 | .circle-step-circle { 21 | margin-top: 0.5em; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /runner/src/server/services/upload/mockUploadService.ts: -------------------------------------------------------------------------------- 1 | import { UploadService } from "./uploadService"; 2 | 3 | export class MockUploadService extends UploadService { 4 | async uploadDocuments(locations: any[]) { 5 | const shouldFailOCR = locations.find( 6 | (location) => location.hapi.filename === "fails-ocr.png" 7 | ); 8 | const responseData = { 9 | res: { 10 | statusCode: 201, 11 | headers: { 12 | location: "https://document-upload-endpoint", 13 | }, 14 | }, 15 | payload: shouldFailOCR && "imageQualityWarning", 16 | }; 17 | 18 | return this.parsedDocumentUploadResponse(responseData); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /runner/test/cases/server/forms/phase-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": {}, 3 | "startPage": "/first-page", 4 | "pages": [ 5 | { 6 | "title": "First page", 7 | "path": "/first-page", 8 | "components": [], 9 | "next": [ 10 | { 11 | "path": "/summary" 12 | } 13 | ] 14 | }, 15 | { 16 | "path": "/summary", 17 | "controller": "./pages/summary.js", 18 | "title": "Summary", 19 | "components": [], 20 | "next": [] 21 | } 22 | ], 23 | "lists": [], 24 | "sections": [], 25 | "conditions": [], 26 | "fees": [], 27 | "outputs": [], 28 | "version": 2, 29 | "skipSummary": false 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | npm-debug.log 5 | .idea 6 | .nyc_output 7 | unit-test.html 8 | package-lock.json 9 | /.yarn/cache/ 10 | */.env 11 | */test-results/* 12 | */test-coverage/* 13 | /runner/tmp.pdf 14 | /model/module/ 15 | /model/dist/ 16 | smoke-tests/designer/reports/ 17 | */dist/ 18 | */.dist/ 19 | tsconfig.tsbuildinfo 20 | .lighthouseci 21 | 22 | .yarn/* 23 | !.yarn/releases 24 | !.yarn/plugins 25 | !.yarn/sdks 26 | !.yarn/versions 27 | .pnp.* 28 | .yarn/build-state.yml 29 | .yarn/install-state.gz 30 | docs/**/typedoc 31 | 32 | /e2e/cypress/screenshots/ 33 | .env_mysql 34 | /queue-model/dist 35 | /queue-model/module 36 | /queue-model/src/prisma/generated -------------------------------------------------------------------------------- /designer/client/conditions/TextValues.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConditionValue } from "@xgovformbuilder/model"; 3 | 4 | export const TextValues = (props) => { 5 | const { updateValue, value } = props; 6 | 7 | const onChangeTextInput = (e) => { 8 | const input = e.target; 9 | const newValue = input.value; 10 | updateValue(new ConditionValue(newValue)); 11 | }; 12 | 13 | return ( 14 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /docs/model/arkit.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://arkit.pro/schema.json", 3 | "excludePatterns": [ 4 | "test/**", 5 | "tests/**", 6 | "**/*.test.*", 7 | "**/*.spec.*", 8 | "webpack.config*" 9 | ], 10 | "components": [ 11 | { 12 | "type": "Model", 13 | "patterns": ["model/src/**/*.ts"], 14 | "excludePatterns": ["node_modules/*", "**/node_modules/*"], 15 | "format": "full" 16 | } 17 | ], 18 | "output": [ 19 | { 20 | "path": "docs/model/architecture-diagram.svg", 21 | "groups": [ 22 | { 23 | "type": "Model", 24 | "components": ["Model"] 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /runner/bin/run/check/getOutOfDateForms.js: -------------------------------------------------------------------------------- 1 | const helper = require("./getJsonFiles"); 2 | const { FORM_PATH, CURRENT_SCHEMA_VERSION } = require("./util"); 3 | const fs = require("fs").promises; 4 | const path = require("path"); 5 | 6 | async function getOutOfDateForms() { 7 | const files = await helper.getJsonFiles(); 8 | let needsMigration = []; 9 | 10 | for (const file of files) { 11 | const form = await fs.readFile(path.join(FORM_PATH, file)); 12 | const version = JSON.parse(form).version || 0; 13 | version < CURRENT_SCHEMA_VERSION && needsMigration.push(file); 14 | } 15 | 16 | return needsMigration; 17 | } 18 | 19 | module.exports = { 20 | getOutOfDateForms, 21 | }; 22 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/fileuploadfield.html: -------------------------------------------------------------------------------- 1 | {% from "file-upload/macro.njk" import govukFileUpload %} 2 | {% macro FileUploadField(component) %} 3 | {{ govukFileUpload(component.model) }} 4 | {% if component.model.value %} 5 |

    You previously uploaded the file ‘{{component.model.value}}’

    6 | {% endif %} 7 | 12 | {% endmacro %} 13 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/models/FormModel.exitOptions.ts: -------------------------------------------------------------------------------- 1 | import { FormModel } from "server/plugins/engine/models/FormModel"; 2 | import { callbackValidation } from "server/plugins/initialiseSession/helpers"; 3 | export class ExitOptions { 4 | url: string; 5 | format?: "STATE" | "WEBHOOK"; 6 | 7 | constructor(exitOptions: FormModel["exitOptions"]) { 8 | const { value, error } = callbackValidation().validate(exitOptions.url); 9 | if (error) { 10 | throw new Error( 11 | `FormModel.exitOptions initialisation failed, ${exitOptions.url} is not on the safelist` 12 | ); 13 | } 14 | this.url = value; 15 | this.format = exitOptions.format; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /submitter/src/submission/types.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@hapi/hapi"; 2 | import { Logger } from "pino"; 3 | import { WebhookService, QueueService } from "./services"; 4 | 5 | type Services = ( 6 | services: string[] 7 | ) => { 8 | webhookService: WebhookService; 9 | queueService: QueueService; 10 | }; 11 | 12 | declare module "@hapi/hapi" { 13 | // Here we are decorating Hapi interface types with 14 | // props from plugins which doesn't export @types 15 | 16 | interface Server { 17 | logger: Logger; 18 | services: Services; // plugin schmervice 19 | registerService: (services: any[]) => void; // plugin schmervice 20 | } 21 | } 22 | 23 | export type HapiServer = Server; 24 | -------------------------------------------------------------------------------- /designer/server/views/includes/home-office-footer.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pluginHandlers/files/prehandlers/getFiles.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | 3 | /** 4 | * Prehandler that parses the payload and finds Buffers (i.e. files). 5 | */ 6 | export function getFiles(request: HapiRequest, _h: HapiResponseToolkit) { 7 | const { uploadService } = request.services([]); 8 | const files = uploadService.fileStreamsFromPayload(request.payload); 9 | if (files.length) { 10 | request.server.logger.info( 11 | { id: request.yar.id, path: request.path }, 12 | `Found ${uploadService.fileSummary(files)} to process on ${request.path}` 13 | ); 14 | return files; 15 | } 16 | 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /runner/src/server/plugins/applicationStatus/checkUserCompletedSummary.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | 3 | export async function checkUserCompletedSummary( 4 | request: HapiRequest, 5 | h: HapiResponseToolkit 6 | ) { 7 | const { cacheService } = request.services([]); 8 | 9 | const state = await cacheService.getState(request); 10 | 11 | if (state?.userCompletedSummary !== true) { 12 | request.logger.error( 13 | [`/${request.params.id}/status`], 14 | `${request.yar.id} user has incomplete state, redirecting to /summary` 15 | ); 16 | return h.redirect(`/${request.params.id}/summary`).takeover(); 17 | } 18 | 19 | return state.userCompletedSummary; 20 | } 21 | -------------------------------------------------------------------------------- /runner/test/cases/server/utils/generateCookiePassword.test.ts: -------------------------------------------------------------------------------- 1 | import * as Code from "@hapi/code"; 2 | import * as Lab from "@hapi/lab"; 3 | import generateCookiePassword from "server/utils/generateCookiePassword"; 4 | 5 | const { expect } = Code; 6 | const lab = Lab.script(); 7 | exports.lab = lab; 8 | const { suite, test } = lab; 9 | 10 | suite("Cookie password generator", () => { 11 | test("Generates a random password 32 characters long", () => { 12 | const password1 = generateCookiePassword(); 13 | const password2 = generateCookiePassword(); 14 | 15 | expect(password1.length).to.equal(32); 16 | expect(password2.length).to.equal(32); 17 | expect(password1).to.not.equal(password2); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/redirect.feature: -------------------------------------------------------------------------------- 1 | Feature: Back link fallback 2 | As a service team, 3 | I want to redirect to another URL 4 | So that part of the service may be completed by another form or url 5 | 6 | Scenario: Redirects to another page 7 | Given the form "redirects" exists 8 | When I navigate to the "redirects" form 9 | And I enter "Turkey" for "Start" 10 | And I continue 11 | Then I see "Cookies are files saved on your phone, tablet or computer when you visit a website." 12 | 13 | Scenario: Continues to next page 14 | Given the form "redirects" exists 15 | When I navigate to the "redirects" form 16 | And I enter "Thailand" for "Start" 17 | And I continue 18 | Then I see "Second page" 19 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pageControllers/index.ts: -------------------------------------------------------------------------------- 1 | export { DobPageController } from "./DobPageController"; 2 | export { HomePageController } from "./HomePageController"; 3 | export { PageController } from "./PageController"; 4 | export { StartDatePageController } from "./StartDatePageController"; 5 | export { StartPageController } from "./StartPageController"; 6 | export { SummaryPageController } from "./SummaryPageController"; 7 | export { PageControllerBase } from "./PageControllerBase"; 8 | export { MiniSummaryPageController } from "./MiniSummaryPageController"; 9 | export { RepeatingSectionSummaryPageController } from "./RepeatingSectionSummaryPageController"; 10 | export { getPageController, controllerNameFromPath } from "./helpers"; 11 | -------------------------------------------------------------------------------- /designer/server/plugins/session.ts: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | import yar from "yar"; 3 | import { ServerRegisterPluginObject } from "@hapi/hapi"; 4 | 5 | export const configureYarPlugin = (): ServerRegisterPluginObject => { 6 | return { 7 | plugin: yar, 8 | options: { 9 | cache: { 10 | expiresIn: config.sessionTimeout, 11 | }, 12 | cookieOptions: { 13 | password: 14 | config.sessionCookiePassword || 15 | Array(32) 16 | .fill(0) 17 | .map(() => Math.random().toString(36).charAt(2)) 18 | .join(""), 19 | isSecure: config.isProd, 20 | isHttpOnly: true, 21 | isSameSite: "Lax", 22 | }, 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /designer/client/components/Icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function SearchIcon(props) { 4 | return ( 5 | 13 | search 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /runner/src/server/plugins/logging.ts: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | import pino from "hapi-pino"; 3 | export default { 4 | plugin: pino, 5 | options: { 6 | prettyPrint: 7 | config.logPrettyPrint === "true" || config.logPrettyPrint === true, 8 | level: config.logLevel, 9 | formatters: { 10 | level: (label) => { 11 | return { level: label }; 12 | }, 13 | }, 14 | debug: config.isDev, 15 | logRequestStart: config.isDev, 16 | logRequestComplete: config.isDev, 17 | ignoreFunc: (_options, request) => 18 | request.path.startsWith("/assets") || request.url.contains("assets"), 19 | redact: { 20 | paths: config.logRedactPaths, 21 | censor: "REDACTED", 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pageControllers/MultiStartPageController.ts: -------------------------------------------------------------------------------- 1 | import { FormData, FormSubmissionErrors } from "../types"; 2 | import { PageController } from "./PageController"; 3 | 4 | export class MultiStartPageController extends PageController { 5 | get viewName() { 6 | return "multi-start-page"; 7 | } 8 | getViewModel(formData: FormData, errors?: FormSubmissionErrors) { 9 | const viewModel = super.getViewModel(formData, errors); 10 | const { showContinueButton, startPageNavigation } = this.pageDef; 11 | return { 12 | ...viewModel, 13 | continueButtonText: showContinueButton && this.pageDef.continueButtonText, 14 | startPageNavigation, 15 | isMultiStartPageController: true, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /designer/client/data/list/__tests__/findList.jest.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | import { findList } from "../findList"; 3 | 4 | const data: FormDefinition = { 5 | conditions: [], 6 | lists: [ 7 | { 8 | name: "listA", 9 | }, 10 | { 11 | name: "listB", 12 | }, 13 | ], 14 | pages: [], 15 | sections: [], 16 | }; 17 | 18 | test("findList throws when no list can be found", () => { 19 | expect(() => findList(data, "listC")).toThrowError( 20 | /No list found with the name/ 21 | ); 22 | }); 23 | 24 | test("findList returns a tuple of the list and the index", () => { 25 | expect(findList(data, "listA")).toEqual([ 26 | { 27 | name: "listA", 28 | }, 29 | 0, 30 | ]); 31 | }); 32 | -------------------------------------------------------------------------------- /designer/server/plugins/blankie.ts: -------------------------------------------------------------------------------- 1 | import Blankie from "blankie"; 2 | import { ServerRegisterPluginObject } from "@hapi/hapi"; 3 | 4 | export const configureBlankiePlugin = (): ServerRegisterPluginObject< 5 | Blankie 6 | > => { 7 | return { 8 | plugin: Blankie, 9 | options: { 10 | defaultSrc: ["self"], 11 | fontSrc: ["self", "data:"], 12 | connectSrc: ["self"], 13 | scriptSrc: [ 14 | "self", 15 | "unsafe-inline", 16 | "unsafe-eval", 17 | "https://unpkg.com/react@16/umd/react.development.js", 18 | "https://unpkg.com/react-dom@16/umd/react-dom.development.js", 19 | ], 20 | styleSrc: ["self"], 21 | imgSrc: ["self", "data:"], 22 | generateNonces: false, 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /designer/client/pages/LandingPage/LandingPage.scss: -------------------------------------------------------------------------------- 1 | .new-config { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: 960px; 5 | margin: 0 auto; 6 | width: 100%; 7 | padding: 20px 30px 200px; 8 | 9 | .choice-wrapper { 10 | margin-top: 70px; 11 | } 12 | 13 | .govuk-fieldset__heading { 14 | font-size: 1.5rem; 15 | font-weight: 700; 16 | } 17 | 18 | .govuk-heading-xl__lowmargin { 19 | margin-bottom: 15px; 20 | } 21 | 22 | .back-link { 23 | margin-bottom: 40px; 24 | } 25 | 26 | .table__cell__noborder { 27 | border-bottom: none; 28 | } 29 | .form-grid { 30 | margin-top: 30px; 31 | } 32 | } 33 | 34 | @media only screen and (min-height: 800px) { 35 | .new-config { 36 | padding-bottom: 200px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /designer/server/lib/persistence/index.ts: -------------------------------------------------------------------------------- 1 | import { S3PersistenceService } from "./s3PersistenceService"; 2 | import { BlobPersistenceService } from "./blobPersistenceService"; 3 | import { StubPersistenceService } from "./persistenceService"; 4 | import { PreviewPersistenceService } from "./previewPersistenceService"; 5 | 6 | type Name = "s3" | "blob" | "preview"; 7 | 8 | export function determinePersistenceService(name: Name, server: any) { 9 | switch (name) { 10 | case "s3": 11 | return () => new S3PersistenceService(server); 12 | case "blob": 13 | return () => new BlobPersistenceService(); 14 | case "preview": 15 | return () => new PreviewPersistenceService(); 16 | default: 17 | return () => new StubPersistenceService(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/designer/startPage.js: -------------------------------------------------------------------------------- 1 | import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I try to create a new form without entering a form name", () => { 4 | cy.findByRole("button", { name: "Next" }).click(); 5 | }); 6 | 7 | Then("I am alerted to the error {string}", (string) => { 8 | cy.findByRole("alert").within(() => { 9 | cy.findByText(string); 10 | }); 11 | }); 12 | 13 | Given("I am on the form designer start page", () => { 14 | cy.visit(`${Cypress.env("DESIGNER_URL")}/app`); 15 | }); 16 | 17 | Then("the form id is not {string}", (string) => { 18 | cy.url().should("not.include", string); 19 | }); 20 | 21 | When("I open Back to previous page", () => { 22 | cy.findByText("Back to previous page").click({ force: true }); 23 | }); 24 | -------------------------------------------------------------------------------- /runner/src/server/services/QueueService.ts: -------------------------------------------------------------------------------- 1 | import { HapiServer } from "server/types"; 2 | 3 | type QueueResponse = [number | string, string | undefined]; 4 | 5 | export abstract class QueueService { 6 | logger: HapiServer["logger"]; 7 | 8 | constructor(server: HapiServer) { 9 | this.logger = server.logger; 10 | } 11 | 12 | /** 13 | * Send data from form submission to submission queue 14 | * @param data 15 | * @param url 16 | * @param allowRetry 17 | * @returns The ID of the newly added row, or undefined in the event of an error 18 | */ 19 | abstract sendToQueue( 20 | data: object, 21 | url: string, 22 | allowRetry?: boolean 23 | ): Promise; 24 | 25 | abstract getReturnRef(rowId: number | string): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /model/src/migration/whichMigrations.ts: -------------------------------------------------------------------------------- 1 | import { migrate as V0_TO_V2 } from "./migration.0-2"; 2 | import { migrate as V1_TO_V2 } from "./migration.1-2"; 3 | import { MigrationScript } from "./types"; 4 | 5 | /** 6 | * Returns which migrations that should be run against your Object with the given version 7 | * @param version 8 | */ 9 | export function whichMigrations(version: number) { 10 | let migrations = new Set(); 11 | switch (version) { 12 | case 0: 13 | migrations.add(V0_TO_V2); 14 | /** 15 | * we are skipping v1 entirely. If we weren't you would do migrations.add([V1_TO_V2, V2_TO_V3]) for example. 16 | */ 17 | break; 18 | case 1: 19 | migrations.add(V1_TO_V2); 20 | break; 21 | } 22 | return migrations; 23 | } 24 | -------------------------------------------------------------------------------- /runner/test/cases/server/forms/phase-none.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": {}, 3 | "startPage": "/first-page", 4 | "pages": [ 5 | { 6 | "title": "First page", 7 | "path": "/first-page", 8 | "components": [], 9 | "next": [ 10 | { 11 | "path": "/summary" 12 | } 13 | ] 14 | }, 15 | { 16 | "path": "/summary", 17 | "controller": "./pages/summary.js", 18 | "title": "Summary", 19 | "components": [], 20 | "next": [] 21 | } 22 | ], 23 | "lists": [], 24 | "sections": [], 25 | "conditions": [], 26 | "fees": [], 27 | "outputs": [], 28 | "version": 2, 29 | "skipSummary": false, 30 | "name": "Phase None form", 31 | "feedback": { 32 | "feedbackForm": false 33 | }, 34 | "phaseBanner": {} 35 | } 36 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pageControllers/HomePageController.ts: -------------------------------------------------------------------------------- 1 | import { PageController } from "./PageController"; 2 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 3 | 4 | export class HomePageController extends PageController { 5 | get getRouteOptions() { 6 | return { 7 | ext: { 8 | onPostHandler: { 9 | method: (_request: HapiRequest, h: HapiResponseToolkit) => { 10 | return h.continue; 11 | }, 12 | }, 13 | }, 14 | }; 15 | } 16 | 17 | get postRouteOptions() { 18 | return { 19 | ext: { 20 | onPostHandler: { 21 | method: (_request: HapiRequest, h: HapiResponseToolkit) => { 22 | return h.continue; 23 | }, 24 | }, 25 | }, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /designer/client/load-form-configurations.js: -------------------------------------------------------------------------------- 1 | import logger from "../client/plugins/logger"; 2 | export function fetchConfigurations() { 3 | return window 4 | .fetch("/api/configurations", { 5 | method: "get", 6 | headers: { 7 | Accept: "application/json", 8 | "Content-Type": "application/json", 9 | }, 10 | }) 11 | .then((res) => { 12 | if (res.ok) { 13 | return res.json(); 14 | } else { 15 | throw res.error; 16 | } 17 | }); 18 | } 19 | 20 | export async function loadConfigurations() { 21 | return await fetchConfigurations() 22 | .then((data) => { 23 | return Object.values(data) || []; 24 | }) 25 | .catch((error) => { 26 | logger.error("loadConfigurations", error); 27 | return []; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /submitter/src/submission/setupDatabase.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import pino from "pino"; 3 | const logger = pino(); 4 | 5 | export function setupDatabase() { 6 | const schemaLocation = require.resolve( 7 | "@xgovformbuilder/queue-model/schema.prisma" 8 | ); 9 | 10 | const child = spawnSync( 11 | "prisma", 12 | ["migrate", "deploy", "--schema", schemaLocation], 13 | { 14 | encoding: "utf-8", 15 | stdio: "inherit", 16 | } 17 | ); 18 | 19 | if (child.status === 1) { 20 | logger.error("Could not connect to database, exiting"); 21 | logger.error(child.error); 22 | process.exit(1); 23 | } 24 | 25 | if (child.stdout) { 26 | logger.info(child.stdout); 27 | logger.info("Database migration was successful, continuing"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /designer/client/data/condition/__tests__/hasConditions.jest.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | import { hasConditions } from "../hasConditions"; 3 | 4 | test("hasCondition returns true when there are conditions", () => { 5 | const data: FormDefinition = { 6 | conditions: [ 7 | { name: "a", displayName: "b", value: { name: "c", conditions: [] } }, 8 | ], 9 | lists: [], 10 | pages: [], 11 | sections: [], 12 | }; 13 | expect(hasConditions(data.conditions)).toBe(true); 14 | }); 15 | 16 | test("hasCondition returns false when there aren't any conditions", () => { 17 | const data: FormDefinition = { 18 | conditions: [], 19 | lists: [], 20 | pages: [], 21 | sections: [], 22 | }; 23 | expect(hasConditions(data.conditions)).toBe(false); 24 | }); 25 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/components/list.html: -------------------------------------------------------------------------------- 1 | {% macro List(params) %} 2 | {% if params.model.type == 'numbered' %} 3 |
      4 | {% else %} 5 |
        6 | {% endif %} 7 | {%- for item in params.model.content %} 8 |
      • {{ item.text | safe }}
      • 9 | {% endfor %} 10 | {% if params.model.type == 'numbered' %} 11 |
    12 | {% else %} 13 | 14 | {% endif %} 15 | {% endmacro %} 16 | 17 | -------------------------------------------------------------------------------- /runner/src/server/plugins/rateLimit.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from "hapi-rate-limit"; 2 | 3 | import { RouteConfig } from "../types"; 4 | 5 | export type RateOptions = { 6 | enabled?: boolean; 7 | userLimit?: number; 8 | }; 9 | 10 | export const configureRateLimitPlugin = (routeConfig?: RouteConfig) => { 11 | return { 12 | plugin: rateLimit, 13 | options: routeConfig 14 | ? routeConfig.rateOptions || { enabled: false } 15 | : { 16 | trustProxy: true, 17 | pathLimit: false, 18 | userLimit: false, 19 | getIpFromProxyHeader: (header) => { 20 | // use the last in the list as this will be the 'real' ELB header 21 | const ips = header.split(","); 22 | return ips[ips.length - 1]; 23 | }, 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /runner/test/cases/server/plugins/engine/EmailAddressField.test.ts: -------------------------------------------------------------------------------- 1 | import * as Code from "@hapi/code"; 2 | import * as Lab from "@hapi/lab"; 3 | import { EmailAddressField } from "src/server/plugins/engine/components"; 4 | 5 | const lab = Lab.script(); 6 | exports.lab = lab; 7 | const { expect } = Code; 8 | const { suite, test } = lab; 9 | 10 | suite("Email address field", () => { 11 | test("Should add 'email' to the autocomplete attribute", () => { 12 | const def = { 13 | name: "myComponent", 14 | title: "My component", 15 | hint: "a hint", 16 | options: {}, 17 | schema: {}, 18 | }; 19 | const emailAddressField = new EmailAddressField(def, {}); 20 | expect(emailAddressField.getViewModel({})).to.contain({ 21 | autocomplete: "email", 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /designer/new-form.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": {}, 3 | "startPage": "/first-page", 4 | "pages": [ 5 | { 6 | "title": "First page", 7 | "path": "/first-page", 8 | "components": [], 9 | "next": [ 10 | { 11 | "path": "/second-page" 12 | } 13 | ] 14 | }, 15 | { 16 | "path": "/second-page", 17 | "title": "Second page", 18 | "components": [], 19 | "next": [ 20 | { 21 | "path": "/summary" 22 | } 23 | ] 24 | }, 25 | { 26 | "title": "Summary", 27 | "path": "/summary", 28 | "controller": "./pages/summary.js", 29 | "components": [] 30 | } 31 | ], 32 | "lists": [], 33 | "sections": [], 34 | "conditions": [], 35 | "fees": [], 36 | "outputs": [], 37 | "version": 2 38 | } 39 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pageControllers/StartPageController.ts: -------------------------------------------------------------------------------- 1 | import { FormData, FormSubmissionErrors } from "../types"; 2 | import { PageController } from "./PageController"; 3 | 4 | export class StartPageController extends PageController { 5 | /** 6 | * The controller which is used when Page["controller"] is defined as "./pages/start.js" 7 | * This page should not be used in production. This page is helpful for prototyping start pages within the app, 8 | * but start pages should really live on gov.uk (whitehall publisher) so a user can be properly signposted. 9 | */ 10 | 11 | getViewModel(formData: FormData, errors?: FormSubmissionErrors) { 12 | return { 13 | ...super.getViewModel(formData, errors), 14 | isStartPage: true, 15 | skipTimeoutWarning: true, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /runner/src/server/views/mini-summary.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block templateImports %} 4 | {{ super() }} 5 | {% endblock %}} 6 | 7 | {% from "error-summary/macro.njk" import govukErrorSummary %} 8 | {% from "partials/components.html" import componentList with context %} 9 | {% from "partials/summary-detail.html" import summaryDetail %} 10 | 11 | {% block content %} 12 |
    13 |
    14 | {% if errors %} 15 | {{ govukErrorSummary(errors) }} 16 | {% endif %} 17 | 18 | {% include "partials/heading.html" %} 19 | 20 | {% for detail in details %} 21 | {{ summaryDetail(detail) }} 22 | {% endfor %} 23 | 24 | {% include "partials/form.html" %} 25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /runner/test/cases/server/forms/phase-alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": {}, 3 | "startPage": "/first-page", 4 | "pages": [ 5 | { 6 | "title": "First page", 7 | "path": "/first-page", 8 | "components": [], 9 | "next": [ 10 | { 11 | "path": "/summary" 12 | } 13 | ] 14 | }, 15 | { 16 | "path": "/summary", 17 | "controller": "./pages/summary.js", 18 | "title": "Summary", 19 | "components": [], 20 | "next": [] 21 | } 22 | ], 23 | "lists": [], 24 | "sections": [], 25 | "conditions": [], 26 | "fees": [], 27 | "outputs": [], 28 | "version": 2, 29 | "skipSummary": false, 30 | "name": "Alpha form", 31 | "feedback": { 32 | "feedbackForm": false 33 | }, 34 | "phaseBanner": { 35 | "phase": "alpha" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/SelectField.ts: -------------------------------------------------------------------------------- 1 | import { ListFormComponent } from "./ListFormComponent"; 2 | import { FormData, FormSubmissionErrors } from "server/plugins/engine/types"; 3 | import { SelectFieldComponent } from "@xgovformbuilder/model"; 4 | import { DataType } from "./types"; 5 | 6 | export class SelectField extends ListFormComponent { 7 | dataType = "list" as DataType; 8 | getViewModel(formData: FormData, errors: FormSubmissionErrors) { 9 | const options: SelectFieldComponent["options"] = this.options; 10 | const viewModel = super.getViewModel(formData, errors); 11 | 12 | viewModel.items = [{ value: "" }, ...(viewModel.items ?? [])]; 13 | if (options.autocomplete) { 14 | viewModel.attributes.autocomplete = options.autocomplete; 15 | } 16 | return viewModel; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/Html.ts: -------------------------------------------------------------------------------- 1 | import { FormData, FormSubmissionErrors } from "../types"; 2 | import { ComponentBase } from "./ComponentBase"; 3 | import config from "../../../config"; 4 | import nunjucks from "nunjucks"; 5 | 6 | export class Html extends ComponentBase { 7 | getViewModel(formData: FormData, errors: FormSubmissionErrors) { 8 | const { options } = this; 9 | let content = this.content; 10 | if (config.allowUserTemplates) { 11 | content = nunjucks.renderString(content, { ...formData }); 12 | } 13 | const viewModel = { 14 | ...super.getViewModel(formData, errors), 15 | content: content, 16 | }; 17 | 18 | if ("condition" in options && options.condition) { 19 | viewModel.condition = options.condition; 20 | } 21 | 22 | return viewModel; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /designer/client/conditions/__tests__/inline-condition-helpers.jest.ts: -------------------------------------------------------------------------------- 1 | import { tryParseInt, isInt } from "../inline-condition-helpers"; 2 | 3 | describe("tryParseInt", () => { 4 | it("it returns a valid integer if one can be parsed", () => { 5 | const result = tryParseInt("2020"); 6 | expect(result).toEqual(2020); 7 | }); 8 | 9 | it("it returns undefined if a valid integer can't be parsed", () => { 10 | const result = tryParseInt(""); 11 | expect(result).toBeUndefined(); 12 | }); 13 | }); 14 | 15 | describe("isInt", () => { 16 | it("it returns true for a valid integer", () => { 17 | const result = isInt("2020"); 18 | expect(result).toEqual(true); 19 | }); 20 | 21 | it("it returns false if not a valid integer", () => { 22 | const result = isInt(""); 23 | expect(result).toEqual(false); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /submitter/src/submission/plugins/retention.ts: -------------------------------------------------------------------------------- 1 | import config from "../../config"; 2 | import { redactSubmissions } from "../retention/redactSubmissions"; 3 | import { R_ERRORS } from "../retention/errors"; 4 | export const pluginRetention = { 5 | name: "retention", 6 | register: async function (server, _options) { 7 | server.route({ 8 | method: "GET", 9 | path: "/retention", 10 | handler: async function (_req, h) { 11 | server.logger.info( 12 | `Deleting records older than ${config.retentionPeriod} days` 13 | ); 14 | 15 | try { 16 | await redactSubmissions(); 17 | return h.response().code(204); 18 | } catch (e) { 19 | server.error(R_ERRORS.RUN_ERROR); 20 | return h.response().code(400); 21 | } 22 | }, 23 | }); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /model/src/migration/migration.0-2.ts: -------------------------------------------------------------------------------- 1 | import { MigrationScript } from "./types"; 2 | 3 | function needsUpgrade(data) { 4 | return !!(data.pages ?? []) 5 | .flatMap((page) => page.components) 6 | .find((component) => component.options?.list); 7 | } 8 | 9 | export function migrate(data): MigrationScript { 10 | if (!needsUpgrade(data)) { 11 | return { ...data, version: 2 }; 12 | } 13 | const { pages } = data; 14 | const newPages = pages.flatMap((page) => { 15 | return page.components.map((component) => { 16 | if (!component.options?.list) { 17 | return component; 18 | } 19 | const { list, ...rest } = component.options; 20 | return { ...component, list, options: { ...rest } }; 21 | }); 22 | }); 23 | 24 | return { 25 | ...data, 26 | pages: newPages, 27 | version: 2, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /docs/runner/arkit.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://arkit.pro/schema.json", 3 | "excludePatterns": [ 4 | "test/**", 5 | "tests/**", 6 | "**/*.test.*", 7 | "**/*.spec.*", 8 | "webpack.config*" 9 | ], 10 | "components": [ 11 | { 12 | "type": "Files", 13 | "patterns": ["runner/src/**/*.ts"], 14 | "excludePatterns": ["node_modules/*", "**/node_modules/*"], 15 | "format": "full" 16 | } 17 | ], 18 | "output": [ 19 | { 20 | "path": "docs/runner/architecture-diagram.svg", 21 | "groups": [ 22 | { 23 | "type": "Engine", 24 | "patterns": ["runner/src/server/plugins/engine/**"] 25 | }, 26 | { 27 | "last": true, 28 | "type": "Server", 29 | "patterns": ["runner/src/server/**"] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /e2e/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | Cypress.on("uncaught:exception", (err, runnable) => { 23 | return false; 24 | }); 25 | -------------------------------------------------------------------------------- /runner/bin/run/check/check.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/nodejs 2 | const helper = require("./getOutOfDateForms"); 3 | const { CliUx } = require("@oclif/core"); 4 | async function check() { 5 | CliUx.ux.action.start("Checking versions of forms in runner/src/forms"); 6 | const files = helper.getOutOfDateForms(); 7 | CliUx.ux.action.stop(); 8 | 9 | if (files.length <= 0) { 10 | CliUx.ux.warn( 11 | `Your form(s) ${files.join( 12 | ", " 13 | )} is/are out of date. Use the designer to upload your files, which runs the migration scripts. Download those JSONs to replace the outdated forms. Migration scripts will not cover conditional reveal fields. You will need to fix those manually.` 14 | ); 15 | process.exit(1); 16 | } else { 17 | CliUx.ux.info("Your forms are up to date"); 18 | } 19 | } 20 | 21 | module.exports = { 22 | check, 23 | }; 24 | -------------------------------------------------------------------------------- /designer/client/components/FormDetails/FormDetailsTitle.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from "react"; 2 | import { Input } from "@govuk-jsx/input"; 3 | import { i18n } from "../../i18n"; 4 | 5 | interface Props { 6 | errors: any; 7 | handleTitleInputBlur: (event: ChangeEvent) => void; 8 | title: string; 9 | } 10 | export const FormDetailsTitle = (props: Props) => { 11 | const { title, errors, handleTitleInputBlur } = props; 12 | 13 | return ( 14 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /runner/src/server/views/partials/summary-detail.html: -------------------------------------------------------------------------------- 1 | {% from "./summary-row.html" import summaryRow %} 2 | 3 | {% macro summaryDetail(data) %} 4 | {% set isRepeatableSection = (data.items[0] | isArray) %} 5 | {% if not isRepeatableSection %} 6 |

    {{data.title}}

    7 | {% endif %} 8 |
    9 | {% for item in data.items %} 10 | {% if not hide %} 11 | {%- if item | isArray %} 12 |

    {{data.title}} {{ loop.index }}

    13 | {% for repeated in item %} 14 | {{ summaryRow(repeated) }} 15 | {% endfor %} 16 | {% else %} 17 | {{ summaryRow(item) }} 18 | {% endif %} 19 | {% endif %} 20 | {% endfor %} 21 |
    22 | {% endmacro %} 23 | -------------------------------------------------------------------------------- /model/src/conditions/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getExpression, 3 | getOperatorConfig, 4 | getOperatorNames, 5 | absoluteDateOrTimeOperatorNames, 6 | relativeDateOrTimeOperatorNames, 7 | } from "./condition-operators"; 8 | 9 | export { 10 | timeUnits, 11 | dateUnits, 12 | dateTimeUnits, 13 | ConditionValue, 14 | DateDirections, 15 | RelativeTimeValue, 16 | conditionValueFrom, 17 | } from "./condition-values"; 18 | 19 | export { ConditionField } from "./condition-field"; 20 | export { Condition } from "./condition"; 21 | export { ConditionRef } from "./condition-ref"; 22 | export { ConditionGroup } from "./condition-group"; 23 | export { ConditionsModel } from "./condition-model"; 24 | export { ConditionGroupDef } from "./condition-group-def"; 25 | export { toExpression, toPresentationString } from "./helpers"; 26 | 27 | export { Coordinator } from "./types"; 28 | -------------------------------------------------------------------------------- /runner/src/server/views/timeout.html: -------------------------------------------------------------------------------- 1 | {% set mainClasses = "govuk-main-wrapper--l" %} 2 | {% set skipTimeoutWarning = true %} 3 | {% from "panel/macro.njk" import govukPanel %} 4 | 5 | {% extends 'layout.html' %} 6 | 7 | {% block content %} 8 |
    9 |
    10 |
    11 |
    12 |

    Your application has timed out

    13 |

    14 | We have reset your application because you did not do anything for {{ sessionTimeout / 60000 }} minutes. We did this 15 | to keep your information secure. 16 |

    17 | Start application again 18 |
    19 |
    20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /designer/client/api/designerApi.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | import logger from "../plugins/logger"; 3 | 4 | export class DesignerApi { 5 | async save(id: string, updatedData: FormDefinition): Promise { 6 | const response = await window.fetch(`/api/${id}/data`, { 7 | method: "put", 8 | body: JSON.stringify(updatedData), 9 | headers: { 10 | Accept: "application/json", 11 | "Content-Type": "application/json", 12 | }, 13 | }); 14 | if (!response.ok) { 15 | throw Error(response.statusText); 16 | } 17 | return response; 18 | } 19 | 20 | async fetchData(id: string) { 21 | try { 22 | const response = await window.fetch(`/api/${id}/data`); 23 | return response.json(); 24 | } catch (e) { 25 | logger.error("fetchData", e); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/backLinkFallback.feature: -------------------------------------------------------------------------------- 1 | Feature: Back link fallback 2 | As a service team, 3 | I want to be able to configure a back link fallback, 4 | so that there is seamless integration between my different services 5 | 6 | As a user, 7 | I want to click the back link, 8 | so that I can return to the previous page or service. 9 | 10 | Scenario: Back link is displayed when there is no history 11 | Given the form "backLinkFallback" exists 12 | When I navigate to the "backLinkFallback" form 13 | Then The back link href is "/help/cookies" 14 | 15 | Scenario: Back link fallback is not used if there is session history 16 | Given the form "backLinkFallback" exists 17 | When I navigate to the "backLinkFallback" form 18 | Then The back link href is "/help/cookies" 19 | When I continue 20 | Then The back link href is "/backLinkFallback/start" -------------------------------------------------------------------------------- /e2e/cypress/fixtures/image-quality-playback.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": {}, 3 | "startPage": "/upload-a-file", 4 | "pages": [ 5 | { 6 | "title": "Upload a file", 7 | "path": "/upload-a-file", 8 | "components": [ 9 | { 10 | "name": "zRpydv", 11 | "options": {}, 12 | "type": "FileUploadField", 13 | "title": "File upload" 14 | } 15 | ], 16 | "next": [{ "path": "/summary" }], 17 | "controller": "UploadPageController" 18 | }, 19 | { 20 | "title": "Summary", 21 | "path": "/summary", 22 | "controller": "./pages/summary.js", 23 | "components": [], 24 | "next": [] 25 | } 26 | ], 27 | "lists": [], 28 | "sections": [], 29 | "conditions": [], 30 | "fees": [], 31 | "outputs": [], 32 | "version": 2, 33 | "skipSummary": false, 34 | "feeOptions": {} 35 | } 36 | -------------------------------------------------------------------------------- /model/src/conditions/condition-group-def.ts: -------------------------------------------------------------------------------- 1 | import { ConditionsArray } from "./types"; 2 | 3 | export class ConditionGroupDef { 4 | first: number; 5 | last: number; 6 | 7 | constructor(first: number, last: number) { 8 | if (typeof first !== "number" || typeof last !== "number") { 9 | throw Error(`Cannot construct a group from ${first} and ${last}`); 10 | } else if (first >= last) { 11 | throw Error(`Last (${last}) must be greater than first (${first})`); 12 | } 13 | 14 | this.first = first; 15 | this.last = last; 16 | } 17 | 18 | contains(index: number) { 19 | return this.first <= index && this.last >= index; 20 | } 21 | 22 | startsWith(index: number) { 23 | return this.first === index; 24 | } 25 | 26 | applyTo(conditions: ConditionsArray) { 27 | return [...conditions].splice(this.first, this.last - this.first + 1); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/files.feature: -------------------------------------------------------------------------------- 1 | Feature: File upload fields 2 | 3 | Background: 4 | Given the form "files" exists 5 | And the form "files-show-filenames-enabled" exists 6 | 7 | 8 | Scenario Outline: showFilenamesOnSummaryPage shows "Uploaded" or the filename correctly on summary pages 9 | And I navigate to the "
    " form 10 | When I upload the file "passes.png" 11 | Then I see "" 12 | Examples: 13 | | form | summaryValue | 14 | | files | Uploaded | 15 | | files-show-filenames-enabled | passes.png | 16 | 17 | 18 | Scenario: Uploading a file shows "You previously uploaded" message 19 | And I navigate to the "files" form 20 | When I upload the file "passes.png" 21 | And I navigate to "/files/file-one" 22 | Then I see "You previously uploaded the file ‘passes.png’" 23 | -------------------------------------------------------------------------------- /model/src/conditions/types.ts: -------------------------------------------------------------------------------- 1 | import { Condition } from "./condition"; 2 | import { ConditionRef } from "./condition-ref"; 3 | import { ConditionGroup } from "./condition-group"; 4 | 5 | export type ConditionsArray = (Condition | ConditionGroup | ConditionRef)[]; 6 | 7 | export enum Coordinator { 8 | AND = "and", 9 | OR = "or", 10 | } 11 | 12 | export type DateTimeUnitValues = 13 | | "years" 14 | | "months" 15 | | "days" 16 | | "hours" 17 | | "minutes" 18 | | "seconds"; 19 | 20 | export type DateUnits = { 21 | YEARS: { display: "year(s)"; value: "years" }; 22 | MONTHS: { display: "month(s)"; value: "months" }; 23 | DAYS: { display: "day(s)"; value: "days" }; 24 | }; 25 | 26 | export type TimeUnits = { 27 | HOURS: { display: "hour(s)"; value: "hours" }; 28 | MINUTES: { display: "minute(s)"; value: "minutes" }; 29 | SECONDS: { display: "second(s)"; value: "seconds" }; 30 | }; 31 | -------------------------------------------------------------------------------- /runner/src/server/plugins/initialiseSession/types.ts: -------------------------------------------------------------------------------- 1 | import { ContentComponentsDef } from "@xgovformbuilder/model"; 2 | 3 | export type InitialiseSession = { 4 | safelist: string[]; 5 | }; 6 | 7 | export type InitialiseSessionOptions = { 8 | callbackUrl: string; 9 | redirectPath?: string; 10 | message?: string; 11 | htmlMessage?: string; 12 | title?: string; 13 | skipSummary?: { 14 | redirectUrl: string; 15 | }; 16 | customText: { 17 | title: string; 18 | paymentSkipped?: false | string; 19 | nextSteps?: false | string; 20 | }; 21 | components: ContentComponentsDef[]; 22 | }; 23 | 24 | export type DecodedSessionToken = { 25 | /** 26 | * Callback url to PUT data to 27 | */ 28 | cb: string; 29 | 30 | /** 31 | * 16 character randomised string 32 | */ 33 | user: string; 34 | 35 | /** 36 | * alias for formId 37 | */ 38 | group: string; 39 | }; 40 | -------------------------------------------------------------------------------- /designer/server/plugins/router.ts: -------------------------------------------------------------------------------- 1 | import { healthCheckRoute } from "./routes"; 2 | 3 | const routes = [ 4 | healthCheckRoute, 5 | { 6 | method: "GET", 7 | path: "/robots.txt", 8 | options: { 9 | handler: { 10 | file: "server/public/static/robots.txt", 11 | }, 12 | }, 13 | }, 14 | { 15 | method: "GET", 16 | path: "/assets/{path*}", 17 | options: { 18 | handler: { 19 | directory: { 20 | path: "./dist/client/assets", 21 | }, 22 | }, 23 | }, 24 | }, 25 | { 26 | method: "GET", 27 | path: "/help/{filename}", 28 | handler: function (request, h) { 29 | return h.view(`help/${request.params.filename}`); 30 | }, 31 | }, 32 | ]; 33 | 34 | export default { 35 | plugin: { 36 | name: "router", 37 | register: (server, _options) => { 38 | server.route(routes); 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/MiniSummaryPageController.feature: -------------------------------------------------------------------------------- 1 | # Created by calum-ukhsa at 29/07/2025 2 | Feature: New page controller that display summaries of parts of the form data 3 | 4 | Scenario: MiniSummaryPageController works as expected 5 | Given the form "mini-summary-fields" exists 6 | And I navigate to the "mini-summary-fields" form 7 | When I enter "Joe" for "First name" 8 | And I enter "Bloggs" for "Last name" 9 | And I continue 10 | And I enter "123" for "Code" 11 | And I continue 12 | Then I see "Check these details are correct before continuing" 13 | And I see "Joe" 14 | And I see "Bloggs" 15 | And I don't see "123" 16 | And I see "Confirm and continue" 17 | Then I click the link "Change First name" 18 | And I enter "Joel" for "First name" 19 | And I continue 20 | Then I see "Joel" 21 | And I continue 22 | Then I see "Fourth page" 23 | -------------------------------------------------------------------------------- /e2e/cypress/support/step_definitions/common/i_enter_the_date_string_in_parts_for_string.js: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then( 4 | "I enter the date {string} in parts for {string}", 5 | (dateString, fieldName) => { 6 | const date = new Date(dateString); 7 | const day = date.getDate(); 8 | const month = date.getMonth() + 1; 9 | const year = date.getFullYear(); 10 | const hour = date.getHours(); 11 | const minute = date.getMinutes(); 12 | 13 | cy.get(`#${fieldName}`).within(() => { 14 | cy.findByLabelText("Day").type(day); 15 | cy.findByLabelText("Month").type(month); 16 | cy.findByLabelText("Year").type(year); 17 | if (hour) { 18 | cy.findByLabelText("Hour").type(hour); 19 | } 20 | 21 | if (minute) { 22 | cy.findByLabelText("Minute").type(minute); 23 | } 24 | }); 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/components/FlashCard.ts: -------------------------------------------------------------------------------- 1 | import { FormData, FormSubmissionErrors } from "../types"; 2 | import { ComponentBase } from "./ComponentBase"; 3 | import { Item, List } from "@xgovformbuilder/model"; 4 | 5 | export class FlashCard extends ComponentBase { 6 | list: List; 7 | get items(): Item[] { 8 | return this.list?.items ?? []; 9 | } 10 | 11 | constructor(def, model) { 12 | super(def, model); 13 | this.list = model.getList(def.list); 14 | } 15 | getViewModel(formData: FormData, errors: FormSubmissionErrors) { 16 | const { items } = this; 17 | const viewModel = super.getViewModel(formData, errors); 18 | 19 | viewModel.content = items.map(({ text, description, condition }) => { 20 | return { 21 | title: text, 22 | text: description || "", 23 | condition, 24 | }; 25 | }); 26 | return viewModel; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /designer/test/testServer.js: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import { rest } from "msw"; 3 | import { setupServer } from "msw/node"; 4 | 5 | const mockedFormConfigurations = [ 6 | { 7 | Key: "Not-a-feedback-form", 8 | DisplayName: "Not a feedback form", 9 | feedbackForm: false, 10 | }, 11 | { 12 | Key: "My-feedback-form", 13 | DisplayName: "My feedback form", 14 | feedbackForm: true, 15 | }, 16 | ]; 17 | 18 | const server = setupServer( 19 | rest.get("/api/configurations", (_req, res, ctx) => { 20 | return res(ctx.json(mockedFormConfigurations)); 21 | }), 22 | 23 | rest.get("*", (req, res, ctx) => { 24 | console.error(`Please add request handler for ${req.url.toString()}`); 25 | return res( 26 | ctx.status(500), 27 | ctx.json({ error: "You must add request handler." }) 28 | ); 29 | }) 30 | ); 31 | 32 | export { server, rest, mockedFormConfigurations }; 33 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/designer/notifyOutput.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("The list {string} should exist", (listName) => { 4 | cy.findByTestId("menu-lists").click(); 5 | cy.findAllByTestId("edit-list").findByText(listName); 6 | cy.findByText("Close").click(); 7 | }); 8 | 9 | When("I open Outputs", () => { 10 | cy.findByTestId("menu-outputs").click(); 11 | }); 12 | 13 | When("I choose Add output", () => { 14 | cy.findByTestId("add-output").click(); 15 | }); 16 | 17 | When("I use the GOVUK notify output type", () => { 18 | cy.findByLabelText("Output type").select("Email via GOVUK Notify"); 19 | }); 20 | 21 | When("I add a personalisation", () => { 22 | cy.findByTestId("add-notify-personalisation").click(); 23 | }); 24 | 25 | Then("{string} should appear in the Description dropdown", (string) => { 26 | cy.contains('[id="link-source"] option', string); 27 | }); 28 | -------------------------------------------------------------------------------- /runner/test/cases/server/plugins/engine/feedback-context-info.test.ts: -------------------------------------------------------------------------------- 1 | import * as Code from "@hapi/code"; 2 | import * as Lab from "@hapi/lab"; 3 | import { 4 | FeedbackContextInfo, 5 | decodeFeedbackContextInfo, 6 | } from "src/server/plugins/engine/feedback"; 7 | 8 | const lab = Lab.script(); 9 | exports.lab = lab; 10 | const { expect } = Code; 11 | const { suite, test } = lab; 12 | 13 | suite("Feedback context info", () => { 14 | test("Should be able to be serialised and deserialised", () => { 15 | const original = new FeedbackContextInfo("My form", "My page", "/badger"); 16 | 17 | expect(decodeFeedbackContextInfo(original.toString())).to.equal(original); 18 | }); 19 | 20 | test("toString should be url friendly", () => { 21 | const original = new FeedbackContextInfo("My form", "My page", "/badger"); 22 | 23 | expect(/^[A-Za-z0-9+/=]*$/.test(original.toString())).to.equal(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /runner/test/cases/server/services/httpService.test.ts: -------------------------------------------------------------------------------- 1 | import * as Code from "@hapi/code"; 2 | import * as Lab from "@hapi/lab"; 3 | import sinon from "sinon"; 4 | import wreck from "@hapi/wreck"; 5 | 6 | import { post } from "server/services/httpService"; 7 | 8 | const { expect } = Code; 9 | const lab = Lab.script(); 10 | exports.lab = lab; 11 | const { afterEach, beforeEach, suite, test } = lab; 12 | 13 | const sandbox = sinon.createSandbox(); 14 | 15 | suite("Http Service", () => { 16 | afterEach(() => { 17 | sinon.restore(); 18 | }); 19 | 20 | test("post request payload format is correct", async () => { 21 | sinon.stub(wreck, "post").returns( 22 | Promise.resolve({ 23 | res: {}, 24 | payload: { reference: "1234" }, 25 | }) 26 | ); 27 | const result = await post("/test", {}); 28 | expect(result).to.equal({ res: {}, payload: { reference: "1234" } }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /submitter/config/default.js: -------------------------------------------------------------------------------- 1 | const { deferConfig } = require("config/defer"); 2 | const dotEnv = require("dotenv"); 3 | if (process.env.NODE_ENV !== "test") { 4 | dotEnv.config({ path: ".env" }); 5 | } 6 | 7 | module.exports = { 8 | env: "development", 9 | port: "9000", 10 | 11 | /** 12 | * logging config 13 | */ 14 | logPrettyPrint: false, 15 | logLevel: "info", 16 | 17 | /** 18 | * Helper flags 19 | */ 20 | isDev: deferConfig(function () { 21 | return this.env !== "production"; 22 | }), 23 | isProd: deferConfig(function () { 24 | return this.env === "production"; 25 | }), 26 | isTest: deferConfig(function () { 27 | return this.env === "test"; 28 | }), 29 | 30 | /** 31 | * Queue service config 32 | */ 33 | queueDatabaseUrl: "", 34 | queueDatabaseUsername: "", 35 | queueDatabasePassword: "", 36 | pollingInterval: "2000", 37 | retentionPeriod: "365", 38 | }; 39 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block templateImports %} 4 | {{ super() }} 5 | {% endblock %}} 6 | 7 | {% from "error-summary/macro.njk" import govukErrorSummary %} 8 | {% from "partials/components.html" import componentList with context %} 9 | 10 | {% block content %} 11 | 12 | {% set gridSize = "full" if components[0].type == 'FlashCard' else "two-thirds" %} 13 |
    14 |
    15 | {% if errors %} 16 | {{ govukErrorSummary(errors) }} 17 | {% endif %} 18 | 19 | {% include "partials/heading.html" %} 20 | 21 | {% if page.hasNext %} 22 | {% include "partials/form.html" %} 23 | {% else %} 24 | {{ componentList(components) }} 25 | {% endif %} 26 | 27 |
    {{ value | dump(2) | safe }}
    28 |
    29 |
    30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /designer/client/reducers/listActions.tsx: -------------------------------------------------------------------------------- 1 | export enum ListActions { 2 | EDIT_LIST = "EDIT_LIST", 3 | 4 | EDIT_TITLE = "EDIT_TITLE", 5 | 6 | SET_SELECTED_LIST = "SET_SELECTED_LIST", 7 | ADD_NEW_LIST = "ADD_NEW_LIST", 8 | 9 | ADD_LIST_ITEM = "ADD_LIST_ITEM", 10 | EDIT_LIST_VALUE_TYPE = "EDIT_LIST_VALUE_TYPE", 11 | EDIT_LIST_ITEM = "EDIT_LIST_ITEM", 12 | DELETE_LIST_ITEM = "DELETE_LIST_ITEM", 13 | 14 | EDIT_LIST_ITEM_TEXT = "EDIT_LIST_ITEM_TEXT", 15 | EDIT_LIST_ITEM_DESCRIPTION = "EDIT_LIST_ITEM_DESCRIPTION", 16 | EDIT_LIST_ITEM_VALUE = "EDIT_LIST_ITEM_VALUE", 17 | EDIT_LIST_ITEM_CONDITION = "EDIT_LIST_ITEM_CONDITION", 18 | SUBMIT_LIST_ITEM = "SUBMIT_LIST_ITEM", 19 | LIST_VALIDATION_ERRORS = "LIST_VALIDATION_ERRORS", 20 | LIST_ITEM_VALIDATION_ERRORS = "LIST_ITEM_VALIDATION_ERRORS", 21 | 22 | SUBMIT = "SUBMIT", 23 | DESELECT_LIST_ITEM = "DESELECT_LIST_ITEM", 24 | SORT_LIST_ITEM = "SORT_LIST_ITEM", 25 | } 26 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/pageControllers/StartDatePageController.ts: -------------------------------------------------------------------------------- 1 | import joi from "joi"; 2 | import { PageController } from "./PageController"; 3 | 4 | /** 5 | * DobPageController adds to the state a users ageGroup 6 | * 7 | * @deprecated FCDO and HO do not use this controller. No guarantee this will work! 8 | */ 9 | export class StartDatePageController extends PageController { 10 | components: any; 11 | 12 | get stateSchema() { 13 | const keys = this.components.getStateSchemaKeys(); 14 | const name = this.components.formItems[0].name; 15 | const d = new Date(); 16 | d.setDate(d.getDate() + 28); 17 | const max = `${d.getMonth() + 1}-${d.getDate()}-${d.getFullYear()}`; 18 | 19 | // Extend the key to validate that the date is 20 | // greater than today and less than today+28 days 21 | keys[name] = keys[name].min("now").max(max); 22 | 23 | return joi.object().keys(keys); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /designer/client/components/FieldEditors/email-edit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | output: any; // TODO 5 | }; 6 | 7 | class EmailEdit extends React.Component { 8 | render() { 9 | const { output } = this.props; 10 | const outputConfiguration = output?.outputConfiguration ?? { 11 | emailAddress: "", 12 | }; 13 | 14 | return ( 15 |
    16 |
    17 | 20 | 27 |
    28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default EmailEdit; 34 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "packageManager": "yarn@3.2.2", 4 | "private": true, 5 | "devDependencies": { 6 | "@badeball/cypress-cucumber-preprocessor": "^13.0.2", 7 | "@cypress/webpack-preprocessor": "^5.12.2", 8 | "@testing-library/cypress": "^8.0.3", 9 | "@testing-library/dom": "^8.17.1", 10 | "@testing-library/user-event": "^14.4.3", 11 | "cypress": "^10.9.0", 12 | "cypress-file-upload": "^5.0.8", 13 | "eslint-plugin-json": "^3.1.0", 14 | "nanoid": "^3.1.23", 15 | "prettier": "2.3.0" 16 | }, 17 | "cypress-cucumber-preprocessor": { 18 | "nonGlobalStepDefinitions": true, 19 | "stepDefinitions": [ 20 | "cypress/e2e/[filepath]/**/*.{js,ts}", 21 | "cypress/e2e/[filepart]/**/*.{js,ts}", 22 | "cypress/e2e/[filepath].{js,ts}", 23 | "cypress/e2e/[filepart].{js,ts}", 24 | "cypress/support/step_definitions/**/*.{js,ts}" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /designer/client/data/page/__tests__/addPage.jest.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | import { addPage } from "../addPage"; 3 | 4 | const data: FormDefinition = { 5 | conditions: [], 6 | lists: [], 7 | name: "", 8 | pages: [ 9 | { 10 | title: "scrambled", 11 | path: "/scrambled", 12 | next: [{ path: "/poached" }], 13 | }, 14 | { title: "poached", path: "/poached" }, 15 | { title: "sunny", path: "/sunny" }, 16 | ], 17 | sections: [], 18 | startPage: "", 19 | }; 20 | 21 | test("addPage throws if a page with the same path already exists", () => { 22 | expect(() => addPage(data, { path: "/scrambled" })).toThrow( 23 | /A page with the path/ 24 | ); 25 | }); 26 | 27 | test("addPage adds a page if one does not exist with the same path", () => { 28 | expect(addPage(data, { path: "/soft-boiled" }).pages).toContainEqual({ 29 | path: "/soft-boiled", 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /model/src/conditions/condition-field.ts: -------------------------------------------------------------------------------- 1 | import { ComponentTypes, ComponentType } from "../components"; 2 | 3 | export class ConditionField { 4 | name: string; 5 | type: ComponentType; 6 | display: string; 7 | 8 | constructor(name: string, type: ComponentType, display: string) { 9 | if (!name || typeof name !== "string") { 10 | throw Error(`name ${name} is not valid`); 11 | } 12 | 13 | if (!ComponentTypes.find((componentType) => componentType.type === type)) { 14 | throw Error(`type ${type} is not valid`); 15 | } 16 | 17 | if (!display || typeof display !== "string") { 18 | throw Error(`display ${display} is not valid`); 19 | } 20 | 21 | this.name = name; 22 | this.type = type; 23 | this.display = display; 24 | } 25 | 26 | static from(obj: { name: string; type: ComponentType; display: string }) { 27 | return new ConditionField(obj.name, obj.type, obj.display); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /model/src/conditions/condition-value-abstract.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "./condition-value-registration"; 2 | 3 | export class ConditionValueAbstract { 4 | type: string; 5 | 6 | constructor(registration: Registration) { 7 | if (new.target === ConditionValueAbstract) { 8 | throw new TypeError("Cannot construct ConditionValue instances directly"); 9 | } 10 | 11 | if (!(registration instanceof Registration)) { 12 | throw new TypeError( 13 | "You must register your value type! Call registerValueType!" 14 | ); 15 | } 16 | 17 | this.type = registration.type; 18 | } 19 | 20 | toPresentationString() { 21 | throw new Error( 22 | "Unsupported Operation. Method toPresentationString have not been implemented" 23 | ); 24 | } 25 | toExpression() { 26 | throw new Error( 27 | "Unsupported Operation. Method toExpression have not been implemented" 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /designer/client/components/ErrorMessage/__tests__/ErrorMessage.jest.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, cleanup, screen } from "@testing-library/react"; 3 | import { ErrorMessage } from ".."; 4 | 5 | describe("ErrorMessage component", () => { 6 | afterEach(cleanup); 7 | 8 | it("renders children text", async () => { 9 | render(Error 123); 10 | expect(screen.findByText("Error 123")).toBeDefined(); 11 | }); 12 | 13 | it("passed down className", async () => { 14 | const { container } = render( 15 | Error 123 16 | ); 17 | expect(container.firstChild).toHaveClass("123"); 18 | }); 19 | 20 | it("renders hidden accessibility error span", () => { 21 | render(Error 123); 22 | expect(screen.getByText("Error:")).toHaveClass("govuk-visually-hidden"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /designer/server/lib/persistence/persistenceService.ts: -------------------------------------------------------------------------------- 1 | import { FormConfiguration } from "@xgovformbuilder/model"; 2 | 3 | export interface PersistenceService { 4 | logger: any; 5 | listAllConfigurations(): Promise; 6 | getConfiguration(id: string): Promise; 7 | uploadConfiguration(id: string, configuration: string): Promise; 8 | copyConfiguration(configurationId: string, newName: string): Promise; 9 | } 10 | 11 | export class StubPersistenceService implements PersistenceService { 12 | logger: any; 13 | uploadConfiguration(_id: string, _configuration: any) { 14 | return Promise.resolve(undefined); 15 | } 16 | 17 | listAllConfigurations() { 18 | return Promise.resolve([]); 19 | } 20 | 21 | getConfiguration(_id: string) { 22 | return Promise.resolve(""); 23 | } 24 | copyConfiguration(_configurationId: string, _newName: string) { 25 | return Promise.resolve(""); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/exit.js: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("the session is initialised for the exit form", () => { 4 | const url = `${Cypress.env("RUNNER_URL")}/session/exit-expiry`; 5 | cy.request("POST", url, { 6 | options: { 7 | callbackUrl: "http://localhost", 8 | redirectPath: "/second-page", 9 | }, 10 | metadata: { 11 | id: "abcdef", 12 | }, 13 | questions: [ 14 | { 15 | fields: [ 16 | { 17 | key: "whichConsulate", 18 | answer: "lisbon", 19 | }, 20 | ], 21 | index: 0, 22 | }, 23 | { 24 | category: "yourDetails", 25 | fields: [ 26 | { 27 | key: "fullName", 28 | answer: "first last", 29 | }, 30 | ], 31 | }, 32 | ], 33 | }).then((res) => { 34 | cy.wrap(res.body.token).as("token"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /runner/src/server/views/partials/summary-row.html: -------------------------------------------------------------------------------- 1 | {% macro summaryRow(item) %} 2 |
    3 |
    4 | {{item.label}} 5 |
    6 |
    7 | {% if item.value %} 8 | {% if item.type == 'FileUploadField' %} 9 | {{ item.filename if item.filename else "Uploaded" }} 10 | {% else %} 11 | {{ item.value | striptags(true) | escape | nl2br }} 12 | {% endif %} 13 | {% else %} 14 | Not supplied 15 | {% endif %} 16 |
    17 |
    18 | {% if item.immutable != true %} 19 | 20 | Change {{item.label}} 21 | 22 | {% endif %} 23 |
    24 |
    25 | {% endmacro %} 26 | -------------------------------------------------------------------------------- /runner/test/cases/server/rate-limit.test.js: -------------------------------------------------------------------------------- 1 | import Lab from "@hapi/lab"; 2 | import { expect } from "@hapi/code"; 3 | 4 | import createServer from "src/server/index"; 5 | 6 | const { suite, before, test, after } = (exports.lab = Lab.script()); 7 | 8 | suite("Rate limit", () => { 9 | let server; 10 | 11 | // Create server before each test 12 | before(async () => { 13 | server = await createServer({ 14 | formFileName: "basic-v1.json", 15 | formFilePath: __dirname, 16 | rateOptions: { userLimit: 1, userCache: { expiresIn: 5000 } }, 17 | }); 18 | server.route({ 19 | method: "GET", 20 | path: "/start", 21 | handler: () => { 22 | return {}; 23 | }, 24 | options: { 25 | plugins: { 26 | "hapi-rate-limit": true, 27 | }, 28 | }, 29 | }); 30 | await server.start(); 31 | }); 32 | after(async () => { 33 | await server.stop(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /designer/server/views/designer.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block pageTitle %} 4 | GOV.UK Site - Designer 5 | {% endblock %} 6 | 7 | {% block header %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | {% if (phase) %} 13 | 23 | {% endif %} 24 | 25 |
    26 |
    27 | {% endblock %} 28 | 29 | {% block footer %} 30 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /runner/src/server/routes/public.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const runnerFolder = path.join(__dirname, "..", "..", ".."); 4 | const govukFolder = path.join( 5 | runnerFolder, 6 | "node_modules", 7 | "govuk-frontend", 8 | "dist", 9 | "govuk" 10 | ); 11 | 12 | export default [ 13 | { 14 | method: "GET", 15 | path: "/assets/{path*}", 16 | options: { 17 | handler: { 18 | directory: { 19 | path: [ 20 | path.join(runnerFolder, "public", "static"), 21 | path.join(runnerFolder, "public", "build"), 22 | govukFolder, 23 | path.join(govukFolder, "assets"), 24 | path.join(govukFolder, "assets", "rebrand"), 25 | path.join( 26 | runnerFolder, 27 | "node_modules", 28 | "hmpo-components", 29 | "assets" 30 | ), 31 | ], 32 | }, 33 | }, 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /designer/client/data/section/__tests__/addSection.jest.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition, Section } from "@xgovformbuilder/model"; 2 | import { addSection } from "../addSection"; 3 | 4 | const data: FormDefinition = { 5 | conditions: [], 6 | lists: [], 7 | pages: [], 8 | sections: [ 9 | { 10 | title: "your details", 11 | name: "yourDetails", 12 | }, 13 | ], 14 | }; 15 | test("addSection throws if a section with the same name already exists", () => { 16 | expect(() => 17 | addSection(data, { name: "yourDetails", title: "your details" }) 18 | ).toThrow(/A section with the name/); 19 | }); 20 | 21 | test("addSection adds a section if the section does not exist", () => { 22 | const newSection: Section = { name: "newSection", title: "new section" }; 23 | expect(addSection(data, newSection).sections).toEqual([ 24 | { 25 | title: "your details", 26 | name: "yourDetails", 27 | }, 28 | newSection, 29 | ]); 30 | }); 31 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/getConditionEvaluationContext.feature: -------------------------------------------------------------------------------- 1 | Feature: Get Condition Evaluation Context 2 | As a forms user 3 | I want to complete a form and return to a previous point and enter different input 4 | So that I can confirm the Condition Evaluation Context is correct 5 | 6 | Scenario: Conditional text is displayed when fulfilling the condition 7 | Given I navigate to the "get-condition-evaluation-context" form 8 | When I choose "Yes" for "Do you have a UK passport?" 9 | And I continue 10 | * I select "1" for "How many applicants are there?" 11 | * I continue 12 | * I enter "Applicant" for "First name" 13 | * I enter "d'egg" for "Surname" 14 | * I continue 15 | Then I see "There Is Someone Called Applicant" 16 | When I go back 17 | And I enter "{selectAll}{backspace}Scrambled" for "First name" 18 | * I continue 19 | * I see "TestConditions" 20 | Then I don't see "There Is Someone Called Applicant" 21 | -------------------------------------------------------------------------------- /designer/client/components/FieldEditors/list-field-edit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ListsEdit from "../../list/ListsEdit"; 4 | import { ListContextProvider } from "../../reducers/listReducer"; 5 | import { ListsEditorContextProvider } from "../../reducers/list/listsEditorReducer"; 6 | import { RenderInPortal } from "../RenderInPortal"; 7 | import ComponentListSelect from "../ComponentListSelect/ComponentListSelect"; 8 | 9 | type Props = { 10 | children: any; // TODO 11 | page: any; // TODO 12 | }; 13 | 14 | function ListFieldEdit({ children, page }: Props) { 15 | return ( 16 | 17 | 18 | 19 | {children} 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export default ListFieldEdit; 29 | -------------------------------------------------------------------------------- /designer/client/data/list/__tests__/addList.jest.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from "@xgovformbuilder/model"; 2 | import { addList } from "../addList"; 3 | 4 | const data: FormDefinition = { 5 | conditions: [], 6 | lists: [ 7 | { 8 | name: "listA", 9 | }, 10 | { 11 | name: "listB", 12 | }, 13 | ], 14 | pages: [], 15 | sections: [], 16 | }; 17 | 18 | test("findList throws when a list with the same name already exists", () => { 19 | expect(() => 20 | addList(data, { name: "listA", title: "list a", items: [], type: "string" }) 21 | ).toThrowError(/A list with the name/); 22 | }); 23 | 24 | test("addList returns a tuple of the list and the index", () => { 25 | expect( 26 | addList(data, { name: "pokedex", title: "151", items: [], type: "number" }) 27 | .lists 28 | ).toEqual([ 29 | { name: "listA" }, 30 | { name: "listB" }, 31 | { items: [], name: "pokedex", title: "151", type: "number" }, 32 | ]); 33 | }); 34 | -------------------------------------------------------------------------------- /designer/server/__tests__/healthCheck.jest.ts: -------------------------------------------------------------------------------- 1 | describe(`/health-check Route`, () => { 2 | const OLD_ENV = process.env; 3 | 4 | test("/health-check route response is correct", async () => { 5 | const options = { 6 | method: "GET", 7 | url: "/health-check", 8 | }; 9 | 10 | process.env = { 11 | ...OLD_ENV, 12 | LAST_COMMIT: "LAST COMMIT", 13 | LAST_TAG: "LAST TAG", 14 | }; 15 | 16 | jest.resetModules(); 17 | await import("../config"); 18 | const { createServer } = await import("../createServer"); 19 | 20 | const server = await createServer(); 21 | const { result } = (await server.inject(options)) as any; 22 | 23 | await server.stop(); 24 | process.env = OLD_ENV; 25 | 26 | expect(result?.status).toEqual("OK"); 27 | expect(result?.lastCommit).toEqual("LAST COMMIT"); 28 | expect(result?.lastTag).toEqual("LAST TAG"); 29 | expect(typeof result?.time).toBe("string"); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /designer/test/.setup.js: -------------------------------------------------------------------------------- 1 | const Adapter = require("enzyme-adapter-react-16"); 2 | const { configure } = require("enzyme"); 3 | const { JSDOM } = require("jsdom"); 4 | 5 | function setUpDomEnvironment() { 6 | const dom = new JSDOM( 7 | ` 8 | 9 | 10 |
    11 | 12 | `, 13 | { url: "http://localhost/" } 14 | ); 15 | const { window } = dom; 16 | 17 | global.window = window; 18 | global.document = window.document; 19 | global.navigator = { 20 | userAgent: "node.js", 21 | }; 22 | copyProps(window, global); 23 | } 24 | 25 | function copyProps(src, target) { 26 | const props = Object.getOwnPropertyNames(src) 27 | .filter((prop) => typeof target[prop] === "undefined") 28 | .map((prop) => Object.getOwnPropertyDescriptor(src, prop)); 29 | Object.defineProperties(target, props); 30 | } 31 | 32 | setUpDomEnvironment(); 33 | 34 | configure({ adapter: new Adapter() }); 35 | -------------------------------------------------------------------------------- /runner/src/server/plugins/engine/services/configurationService.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { idFromFilename } from "../helpers"; 5 | 6 | const FORMS_FOLDER = path.join(__dirname, "..", "..", "..", "forms"); 7 | 8 | export type FormConfiguration = { 9 | configuration: any; // TODO 10 | id: string; 11 | }; 12 | 13 | /** 14 | * Reads the runner/src/server/forms directory for JSON files. The forms that are found will be loaded up at localhost:3009/id 15 | */ 16 | export const loadPreConfiguredForms = (): FormConfiguration[] => { 17 | const configFiles = fs 18 | .readdirSync(FORMS_FOLDER) 19 | .filter((filename: string) => filename.indexOf(".json") >= 0); 20 | 21 | return configFiles.map((configFile) => { 22 | const dataFilePath = path.join(FORMS_FOLDER, configFile); 23 | const configuration = require(dataFilePath); 24 | const id = idFromFilename(configFile); 25 | return { configuration, id }; 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /submitter/src/submission/plugins/poll.ts: -------------------------------------------------------------------------------- 1 | import config from "../../config"; 2 | import { QueueService } from "../services"; 3 | import { Logger } from "pino"; 4 | 5 | export const pluginPoll = { 6 | name: "poll", 7 | register: async function (server, _options) { 8 | const { queueService } = server.services([]); 9 | await poll(queueService, server.logger); 10 | }, 11 | }; 12 | 13 | async function poll(queueService: QueueService, logger: Logger) { 14 | const submission = await queueService.getSubmissions(); 15 | if (!submission) { 16 | logger.info(["poll"], "No unprocessed submissions found. Continuing"); 17 | setTimeout(async () => { 18 | await poll(queueService, logger); 19 | }, config.pollingInterval); 20 | } else { 21 | logger.info( 22 | ["poll"], 23 | `Unprocessed submission found. Row ref: ${submission.id}` 24 | ); 25 | await queueService.submit(submission); 26 | await poll(queueService, logger); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/repeatField/samePageSummary.feature: -------------------------------------------------------------------------------- 1 | Feature: Repeat field - separate page summary 2 | As a user, 3 | I want to see the summary of my repeat field entries on a different page, 4 | so that not too much information is displayed 5 | 6 | Scenario: Only same page summary is displayed 7 | Given the form "repeat-field-same-page" exists 8 | And I navigate to the "repeat-field-same-page" form 9 | When I enter "French{enter}" for "Which languages do you translate or interpret?" 10 | And I select the button "Add to list" 11 | And I enter "Italian{enter}" for "Which languages do you translate or interpret?" 12 | And I select the button "Add to list" 13 | Then I see "French" 14 | And I see "Italian" 15 | When I continue 16 | Then I see "Check your answers" 17 | # Final summary page 18 | And I see "French, Italian" 19 | # Mini summary not displayed separately 20 | And I don't see "You have selected these languages" 21 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/runner/repeatField/separatePageSummary.feature: -------------------------------------------------------------------------------- 1 | Feature: Repeat field - same page summary 2 | As a user, 3 | I want to see the summary of my repeat field entries on the same page, 4 | so that I can review my answers as I add them 5 | 6 | Scenario: Only separate page summary is displayed 7 | Given the form "repeat-field-separate-page" exists 8 | And I navigate to the "repeat-field-separate-page" form 9 | When I enter "French{enter}" for "Which languages do you translate or interpret?" 10 | And I select the button "Continue" 11 | And I select the button "Add another" 12 | And I enter "Italian{enter}" for "Which languages do you translate or interpret?" 13 | Then I don't see "French" 14 | When I select the button "Continue" 15 | Then I see "You have selected these languages" 16 | Then I see "French" 17 | And I see "Italian" 18 | When I continue 19 | Then I see "Check your answers" 20 | And I see "French, Italian" 21 | -------------------------------------------------------------------------------- /runner/src/server/plugins/applicationStatus/retryPay.ts: -------------------------------------------------------------------------------- 1 | import { HapiRequest, HapiResponseToolkit } from "server/types"; 2 | import { FormModel } from "server/plugins/engine/models"; 3 | 4 | export async function retryPay(request: HapiRequest, h: HapiResponseToolkit) { 5 | const { statusService } = request.services([]); 6 | const shouldShowPayErrorPage = await statusService.shouldShowPayErrorPage( 7 | request 8 | ); 9 | 10 | const form: FormModel = request.server.app.forms[request.params.id]; 11 | const feeOptions = form.feeOptions; 12 | const { 13 | allowSubmissionWithoutPayment = true, 14 | customPayErrorMessage, 15 | } = feeOptions; 16 | if (shouldShowPayErrorPage) { 17 | return h 18 | .view("pay-error", { 19 | errorList: ["there was a problem with your payment"], 20 | allowSubmissionWithoutPayment, 21 | customPayErrorMessage, 22 | }) 23 | .takeover(); 24 | } 25 | 26 | return shouldShowPayErrorPage; 27 | } 28 | -------------------------------------------------------------------------------- /e2e/cypress/e2e/designer/accessibilityStatement.feature: -------------------------------------------------------------------------------- 1 | Feature: Footer links 2 | As a forms designer 3 | I want to be able to view the cookies, accessibility statement and terms & conditions 4 | So that I am aware of how my data is used 5 | 6 | Background: 7 | Given I am on the new configuration page 8 | 9 | Scenario: The cookies link opens a tab to the cookies page 10 | When I select "Cookies" in the footer 11 | Then I see "Cookies are files" 12 | 13 | Scenario: The accessibility statement link opens a tab to the accessibility statement page 14 | When I select "Accessibility Statement" in the footer 15 | Then I see "This statement applies to the Digital Form Designer website" 16 | 17 | Scenario: The terms and conditions statement link opens a tab to the terms and conditions page 18 | Given I am on the new configuration page 19 | When I select "Terms and Conditions" in the footer 20 | Then I see "By using this digital service you agree to our" 21 | 22 | -------------------------------------------------------------------------------- /model/src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | export const serialiseAndDeserialise = (obj: T): T => { 4 | if (typeof obj === "object" && obj !== null) { 5 | return JSON.parse(JSON.stringify(obj)); 6 | } 7 | 8 | return obj; 9 | }; 10 | 11 | export const clone = (obj: T & { clone?: () => T }): T => { 12 | if (obj) { 13 | if (typeof obj.clone === "function") { 14 | return obj.clone(); 15 | } 16 | 17 | return serialiseAndDeserialise(obj); 18 | } 19 | return obj; 20 | }; 21 | 22 | export function filter( 23 | obj: T, 24 | predicate: (value: any) => boolean 25 | ): Partial { 26 | const result = {}; 27 | 28 | for (const [key, value] of Object.entries(obj)) { 29 | if (value && predicate(value)) { 30 | result[key] = value; 31 | } 32 | } 33 | 34 | return result; 35 | } 36 | 37 | export const nanoid = customAlphabet( 38 | "0123456789_abcdefghijklmnopqrstuvwxyz", 39 | 6 40 | ); 41 | -------------------------------------------------------------------------------- /runner/test/cases/server/plugins/engine/services/configurationService.test.ts: -------------------------------------------------------------------------------- 1 | import * as Code from "@hapi/code"; 2 | import * as Lab from "@hapi/lab"; 3 | import { loadPreConfiguredForms } from "src/server/plugins/engine/services/configurationService"; 4 | 5 | const lab = Lab.script(); 6 | exports.lab = lab; 7 | const { expect } = Code; 8 | const { suite, test } = lab; 9 | 10 | suite("Engine Plugin ConfigurationService", () => { 11 | test("it loads pre-configured forms configuration correctly ", () => { 12 | const testFormJSON = require("../../../../../../src/server/forms/test.json"); 13 | const reportFormJSON = require("../../../../../../src/server/forms/report-a-terrorist.json"); 14 | const result = loadPreConfiguredForms(); 15 | 16 | expect(result).to.contain([ 17 | { 18 | configuration: testFormJSON, 19 | id: "test", 20 | }, 21 | { 22 | id: "report-a-terrorist", 23 | configuration: reportFormJSON, 24 | }, 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /designer/client/data/page/updateLink.ts: -------------------------------------------------------------------------------- 1 | import { ConditionRawData, FormDefinition } from "@xgovformbuilder/model"; 2 | import { Path } from "../../reducers/data/types"; 3 | import { findPage } from "./findPage"; 4 | 5 | export function updateLink( 6 | data: FormDefinition, 7 | from: Path, 8 | to: Path, 9 | condition?: ConditionRawData["name"] 10 | ): FormDefinition { 11 | const [fromPage, fromPageIndex] = findPage(data, from); 12 | findPage(data, to); 13 | const existingLinkIndex = 14 | fromPage.next?.findIndex((next) => next.path === to) ?? -1; 15 | if (existingLinkIndex < 0) { 16 | throw Error("Could not find page or links to update"); 17 | } 18 | 19 | const updatedNext = [...fromPage.next!]; 20 | updatedNext[existingLinkIndex] = { 21 | ...updatedNext[existingLinkIndex], 22 | condition, 23 | }; 24 | const updatedPage = { ...fromPage, next: updatedNext }; 25 | 26 | const pages = [...data.pages]; 27 | pages[fromPageIndex] = updatedPage; 28 | return { ...data, pages }; 29 | } 30 | --------------------------------------------------------------------------------