From 12213629b65a34335e1cec69d88615daddeb76df Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:04:19 +0000 Subject: [PATCH 01/11] chore: set up Epsom and Ewell Power Automate tokens (#4070) --- .env.example | 1 + api.planx.uk/.env.test.example | 1 + api.planx.uk/modules/auth/middleware.ts | 4 ++++ doc/how-to/aws/how-to-setup-aws-s3-submissions.md | 10 ++++++---- doc/how-to/secrets/how-to-add-a-team-secret.md | 1 + doc/how-to/secrets/how-to-generate-a-secret.md | 2 +- docker-compose.yml | 1 + infrastructure/application/Pulumi.production.yaml | 2 ++ infrastructure/application/Pulumi.staging.yaml | 6 ++++-- infrastructure/application/index.ts | 4 ++++ 10 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index fbdd5ecd11..766aa593e0 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,7 @@ FILE_API_KEY_NEXUS=👻 FILE_API_KEY_BARNET=👻 FILE_API_KEY_LAMBETH=👻 FILE_API_KEY_SOUTHWARK=👻 +FILE_API_KEY_EPSOM_EWELL=👻 # Editor EDITOR_URL_EXT=http://localhost:3000 diff --git a/api.planx.uk/.env.test.example b/api.planx.uk/.env.test.example index ec234e54f9..4df97c1b50 100644 --- a/api.planx.uk/.env.test.example +++ b/api.planx.uk/.env.test.example @@ -22,6 +22,7 @@ FILE_API_KEY_NEXUS=👻 FILE_API_KEY_BARNET=👻 FILE_API_KEY_LAMBETH=👻 FILE_API_KEY_SOUTHWARK=👻 +FILE_API_KEY_EPSOM_EWELL=👻 # Editor EDITOR_URL_EXT=example.com diff --git a/api.planx.uk/modules/auth/middleware.ts b/api.planx.uk/modules/auth/middleware.ts index 9bd5d63ea7..d831e9a557 100644 --- a/api.planx.uk/modules/auth/middleware.ts +++ b/api.planx.uk/modules/auth/middleware.ts @@ -98,6 +98,10 @@ export const useFilePermission: RequestHandler = (req, _res, next): void => { isEqual( req.headers["api-key"] as string, process.env.FILE_API_KEY_SOUTHWARK!, + ) || + isEqual( + req.headers["api-key"] as string, + process.env.FILE_API_KEY_EPSOM_EWELL!, ); if (!isAuthenticated) return next({ status: 401, message: "Unauthorised" }); return next(); diff --git a/doc/how-to/aws/how-to-setup-aws-s3-submissions.md b/doc/how-to/aws/how-to-setup-aws-s3-submissions.md index 998f963a72..43c2949824 100644 --- a/doc/how-to/aws/how-to-setup-aws-s3-submissions.md +++ b/doc/how-to/aws/how-to-setup-aws-s3-submissions.md @@ -12,7 +12,7 @@ Once a council has confirmed they're cleared from their IT dept to use this meth 2. Create 2x tokens for sending secure requests to the Power Automate webhook and add both encrypted values to: - `team_integrations.production_power_automate_api_key` & `team_integrations.staging_power_automate_api_key` via the production Hasura console - - See `how-to-generate-a-secret` for how to properly generate tokens and encrypt values + - See `how-to-generate-a-secret` and `how-to-add-a-team-secret` for how to properly generate tokens and encrypt values 3. Create 2x tokens for downloading files from the PlanX S3 Bucket and add values to: - Root `.env.example` & `.env`, API's `.env.test` & `.env.test.example` as `FILE_API_KEY_{TEAM_SLUG}` @@ -20,7 +20,9 @@ Once a council has confirmed they're cleared from their IT dept to use this meth - Root `docker-compose.yml` - API's `modules/auth/middleware.ts` function `isAuthenticated` - Pulumi's `infrastructure/application/index.ts` list of `apiService` "environment" variables - - Run `pulumi config set file-api-key-{team_slug} --stack {stack}` 2x for each staging & production stacks - - Encrypt the values using _our_ encrypt scripts (again see `how-to-generate-a-secret`) and add to `team_integrations.production_file_api_key` & `team_integrations.staging_file_api_key` via the production Hasura console. Please note these values are _not_ currently read, but suitable for a potential future refactor (just a bit tricky because file API keys are issued to a mix of _teams_ and _systems_ (eg BOPS & Idox)). + - Run `pulumi config set file-api-key-{team_slug} {your-new-secret} --secret --stack {stack}` once for each staging & production stacks, making sure that the secret you used for the root `.env` is the STAGING secret. + - Encrypt the values using _our_ encrypt scripts (see [`how-to-add-a-team-secret.md`](https://github.com/theopensystemslab/planx-new/blob/main/doc/how-to/secrets/how-to-add-a-team-secret.md)) and add to `team_integrations.production_file_api_key` & `team_integrations.staging_file_api_key` via the production Hasura console. Please note these values are _not_ currently read, but suitable for a potential future refactor (just a bit tricky because file API keys are issued to a mix of _teams_ and _systems_ (eg BOPS & Idox)). -4. Securely share tokens back to council contact via onetimesecret or similar +4. Securely share all four raw tokens back to council contact via onetimesecret or similar: +- The two Power Automate API keys +- The two file API keys diff --git a/doc/how-to/secrets/how-to-add-a-team-secret.md b/doc/how-to/secrets/how-to-add-a-team-secret.md index dfed00ba3f..ab1b13555a 100644 --- a/doc/how-to/secrets/how-to-add-a-team-secret.md +++ b/doc/how-to/secrets/how-to-add-a-team-secret.md @@ -17,6 +17,7 @@ This guide will demonstrate how to - > [!NOTE] > The `stack_name` should be either `production` or `staging`, depending on which environment the secret is for (e.g. `staging_govpay_secret`). +> I.e. the staging encryption key needs to be used for staging secrets (even though they need to get set on the production database) ### Encrypt the secret 1. In `/scripts/encrypt`, run the encryption script using the encryption key and raw secret that you obtained in the previous steps: `pnpm encrypt `. diff --git a/doc/how-to/secrets/how-to-generate-a-secret.md b/doc/how-to/secrets/how-to-generate-a-secret.md index afe1abea4a..01839a36f3 100644 --- a/doc/how-to/secrets/how-to-generate-a-secret.md +++ b/doc/how-to/secrets/how-to-generate-a-secret.md @@ -5,7 +5,7 @@ 2. Secrets must meet the following criteria - * Have a minimum length of 32 characters * All characters should be alphanumeric -3. Follow [the current process for adding secrets](https://github.com/theopensystemslab/planx-new/blob/main/doc/how-to/how-to-add-a-secret.md) to the application +3. Follow [the current process for adding secrets](https://github.com/theopensystemslab/planx-new/blob/main/doc/how-to/secrets/how-to-add-a-secret.md) to the application ## Principles - Staging and Production environments should not share secrets diff --git a/docker-compose.yml b/docker-compose.yml index e846bfee9d..34a4c69e8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,6 +127,7 @@ services: FILE_API_KEY_BARNET: ${FILE_API_KEY_BARNET} FILE_API_KEY_LAMBETH: ${FILE_API_KEY_LAMBETH} FILE_API_KEY_SOUTHWARK: ${FILE_API_KEY_SOUTHWARK} + FILE_API_KEY_EPSOM_EWELL: ${FILE_API_KEY_EPSOM_EWELL} FILE_API_KEY_NEXUS: ${FILE_API_KEY_NEXUS} FILE_API_KEY: ${FILE_API_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} diff --git a/infrastructure/application/Pulumi.production.yaml b/infrastructure/application/Pulumi.production.yaml index ba75a36fc7..1abd12ce1e 100644 --- a/infrastructure/application/Pulumi.production.yaml +++ b/infrastructure/application/Pulumi.production.yaml @@ -10,6 +10,8 @@ config: secure: AAABAGyTfLujGho+V0tEhFXQRET5FjYK6txyaFTB3gY/VaKzq8yNlocJTAM5nt8mBhF6T+AeQD2GxW63 application:file-api-key-barnet: secure: AAABANMl+fVFsRVGXvJV/aLManXO+TldXVDhp5QH6KGWJoG7O9Ket63zIW1iOiinINWJ2I5OizI= + application:file-api-key-epsom-ewell: + secure: AAABANvwhiVRBq8NH7ZqcToUzYn4X+KfC5Wm8WjWUKXT5TuVXqC6zHhVVKFBbmdtKjC4j5M4+bWsLiFO9dO0MLobxLpK7YCE application:file-api-key-lambeth: secure: AAABAMNhdCTlFx3fZH/nO71ildypZB2JR5NixlQCENsS1VqwdiOX17q/Gi1UFrCQi2qaY2sZFG4= application:file-api-key-nexus: diff --git a/infrastructure/application/Pulumi.staging.yaml b/infrastructure/application/Pulumi.staging.yaml index be082e9ca8..5c34389e81 100644 --- a/infrastructure/application/Pulumi.staging.yaml +++ b/infrastructure/application/Pulumi.staging.yaml @@ -11,6 +11,8 @@ config: secure: AAABAN0LjLOgxCkr5ZqQLn6FkZPcrPlvNG4fbNZ02W2qC1VVYVee/3aToZQuXuokVwnIPNbbe2w= application:file-api-key-barnet: secure: AAABAFpZq81zy3CKFXUgi9oEGIGp7LDVD3TNlYkZD4liX0bxOrmMJYdDpMmyGt4aGARF63nEUmo= + application:file-api-key-epsom-ewell: + secure: AAABAD1/nlJ2EOEglLiiNsOLbOd3KWCONhNhJAIdZQVnrSRsNIzX2luszOreQf20EYl8AZ4L1TiheqUHSt22e5z1FiLWoCtY application:file-api-key-lambeth: secure: AAABALQTeIf/uScxASJkhmoPRhewQT94Guad4iJ7GRk0DcND8wDUG0eNxDU4+XwUQZqCnL2DP+E= application:file-api-key-nexus: @@ -36,6 +38,8 @@ config: secure: AAABACbmLC4176IBxX5iL64/nycSXEsCYSQ0hTb7t2OCVlWUc627Vr/EpBhcqPrw9q+0z8UOvRJG5/c/DflZxfPxyJRUVNu+ application:mapbox-access-token: secure: AAABAMWf2zVq5/mKCLynpgzAidNsnbUEBpb47n7MRWp2xzRgwaf3kzOvnZax9N04ZScQqU6I5/tEKTBAbSb8MIBJ8mU2iTZbPg8FD6wYsetRyftm1K39KBsIl9aS7fXvZFOG7BsC4qMDEhlDkH8gbV2HTev3VvvRUe3lzVhjNGNHqQ== + application:metabase-api-key: + secure: AAABAFf+hW09AWupsY6adrPAJHCkrTMeRX7/gaUHLYXi3QS77MVelPp9K0L4zyUL8u7zDanjuE9G/bfIlcVXLwiLLKAJkt5to9knKJqTXg== application:metabase-encryption-secret-key: secure: AAABAGmfVICD8sR+IE6mHC8BNUY1WQXGCbv5F3C1fSgA+1ADiRem3GNrwY0YRZociRYuPIo3MIRS0aIg44jt10SBCE0ik58wHamcKA== application:metabasePgPassword: @@ -66,5 +70,3 @@ config: certificates:cloudflare-zone-id: dc27ac531ff8862559ed9ab5016c4953 cloudflare:apiToken: secure: AAABABWhDm+7RstbxLXd1D8CcxkylHS6UKMqk4kOaY7Y0E7FJS4bZfvyGs0nks80hl3vjENH4eDuFbUgA82/sA4SmDlfpNXr - application:metabase-api-key: - secure: AAABAFf+hW09AWupsY6adrPAJHCkrTMeRX7/gaUHLYXi3QS77MVelPp9K0L4zyUL8u7zDanjuE9G/bfIlcVXLwiLLKAJkt5to9knKJqTXg== diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index c0cd37a8a4..96cd773299 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -359,6 +359,10 @@ export = async () => { name: "FILE_API_KEY_SOUTHWARK", value: config.requireSecret("file-api-key-southwark"), }, + { + name: "FILE_API_KEY_EPSOM_EWELL", + value: config.requireSecret("file-api-key-epsom-ewell"), + }, { name: "GOOGLE_CLIENT_ID", value: config.require("google-client-id"), From f446f0bc1eff9d989721165c10e51fa3ef90bcf6 Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:49:37 +0000 Subject: [PATCH 02/11] refactor: create baseOptionsEditor and delete duplicates (#4041) --- .../components/Checklist/Editor/Options.tsx | 6 +- .../Checklist/Editor/OptionsEditor.tsx | 132 +++++------------- .../Checklist/Public/tests/Public.test.tsx | 16 +-- .../src/@planx/components/Checklist/types.ts | 9 -- .../src/@planx/components/Question/Editor.tsx | 99 +------------ .../components/Question/OptionsEditor.tsx | 23 +++ .../components/shared/BaseOptionsEditor.tsx | 109 +++++++++++++++ 7 files changed, 182 insertions(+), 212 deletions(-) create mode 100644 editor.planx.uk/src/@planx/components/Question/OptionsEditor.tsx create mode 100644 editor.planx.uk/src/@planx/components/shared/BaseOptionsEditor.tsx diff --git a/editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx b/editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx index 0080d90a92..388c021dac 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx +++ b/editor.planx.uk/src/@planx/components/Checklist/Editor/Options.tsx @@ -14,7 +14,7 @@ import InputRow from "ui/shared/InputRow"; import { Option } from "../../shared"; import type { Group } from "../model"; -import { OptionEditor } from "./OptionsEditor"; +import ChecklistOptionsEditor from "./OptionsEditor"; export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => { return ( @@ -70,7 +70,7 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => { }) as Option } newValueLabel="add new option" - Editor={OptionEditor} + Editor={ChecklistOptionsEditor} editorExtraProps={{ groupIndex, showValueField: !!formik.values.fn, @@ -142,7 +142,7 @@ export const Options: React.FC<{ formik: FormikHookReturn }> = ({ formik }) => { }, }) as Option } - Editor={OptionEditor} + Editor={ChecklistOptionsEditor} editorExtraProps={{ showValueField: !!formik.values.fn }} /> )} diff --git a/editor.planx.uk/src/@planx/components/Checklist/Editor/OptionsEditor.tsx b/editor.planx.uk/src/@planx/components/Checklist/Editor/OptionsEditor.tsx index 67bff66212..bb4dfb6e41 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/Editor/OptionsEditor.tsx +++ b/editor.planx.uk/src/@planx/components/Checklist/Editor/OptionsEditor.tsx @@ -1,104 +1,46 @@ +import { + BaseOptionsEditor, + BaseOptionsEditorProps, +} from "@planx/components/shared/BaseOptionsEditor"; import React from "react"; -import ImgInput from "ui/editor/ImgInput/ImgInput"; import SimpleMenu from "ui/editor/SimpleMenu"; -import Input from "ui/shared/Input/Input"; -import InputRow from "ui/shared/InputRow"; -import InputRowItem from "ui/shared/InputRowItem"; -import { FlagsSelect } from "../../shared/FlagsSelect"; -import { OptionEditorProps } from "../types"; +export type ChecklistOptionsEditorProps = BaseOptionsEditorProps & { + index: number; + groupIndex?: number; + groups?: Array; + onMoveToGroup?: (itemIndex: number, groupIndex: number) => void; + showValueField?: boolean; +}; -export const OptionEditor: React.FC = (props) => { +const ChecklistOptionsEditor: React.FC = ({ + value, + onChange, + showValueField = false, + groups, + onMoveToGroup, + index, +}) => { return ( -
- - {props.value.id ? ( - - ) : null} - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - text: ev.target.value, - }, - }); - }} - placeholder="Option" - /> - - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - img, - }, - }); - }} + + {typeof index !== "undefined" && groups && onMoveToGroup && ( + ({ + label: `Move to ${group || `group ${groupIndex}`}`, + onClick: () => { + if (onMoveToGroup && typeof index === "number") + onMoveToGroup(index, groupIndex); + }, + disabled: groupIndex === groupIndex, + }))} /> - - {typeof props.index !== "undefined" && - props.groups && - props.onMoveToGroup && ( - ({ - label: `Move to ${group || `group ${groupIndex}`}`, - onClick: () => { - props.onMoveToGroup && - typeof props.index === "number" && - props.onMoveToGroup(props.index, groupIndex); - }, - disabled: groupIndex === props.groupIndex, - }))} - /> - )} - - - {props.showValueField && ( - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - val: ev.target.value, - }, - }); - }} - /> - )} - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - flag: ev, - }, - }); - }} - /> -
+ ); }; + +export default ChecklistOptionsEditor; diff --git a/editor.planx.uk/src/@planx/components/Checklist/Public/tests/Public.test.tsx b/editor.planx.uk/src/@planx/components/Checklist/Public/tests/Public.test.tsx index f77d90ae1b..c7a6804537 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/Public/tests/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/Checklist/Public/tests/Public.test.tsx @@ -22,7 +22,7 @@ describe("Checklist Component - Grouped Layout", () => { text="home type?" handleSubmit={handleSubmit} groupedOptions={groupedOptions} - />, + /> ); await user.click(screen.getByText("Section 1")); @@ -46,7 +46,7 @@ describe("Checklist Component - Grouped Layout", () => { handleSubmit={handleSubmit} previouslySubmittedData={{ answers: ["S1_Option1", "S3_Option1"] }} groupedOptions={groupedOptions} - />, + /> ); expect(screen.getByTestId("group-0-expanded")).toBeTruthy(); @@ -67,7 +67,7 @@ describe("Checklist Component - Grouped Layout", () => { description="" text="home type?" groupedOptions={groupedOptions} - />, + /> ); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -83,7 +83,7 @@ describe("Checklist Component - Grouped Layout", () => { text="home type?" handleSubmit={handleSubmit} groupedOptions={groupedOptions} - />, + /> ); const [section1Button, section2Button, section3Button] = screen.getAllByRole("button"); @@ -140,7 +140,7 @@ describe("Checklist Component - Basic & Images Layout", () => { text="home type?" handleSubmit={handleSubmit} options={options[type]} - />, + /> ); expect(screen.getByRole("heading")).toHaveTextContent("home type?"); @@ -177,7 +177,7 @@ describe("Checklist Component - Basic & Images Layout", () => { handleSubmit={handleSubmit} previouslySubmittedData={{ answers: ["flat_id", "house_id"] }} options={options[type]} - />, + /> ); await user.click(screen.getByTestId("continue-button")); @@ -194,7 +194,7 @@ describe("Checklist Component - Basic & Images Layout", () => { description="" text="home type?" options={options[type]} - />, + /> ); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -210,7 +210,7 @@ describe("Checklist Component - Basic & Images Layout", () => { text="home type?" handleSubmit={handleSubmit} options={options[type]} - />, + /> ); await user.tab(); diff --git a/editor.planx.uk/src/@planx/components/Checklist/types.ts b/editor.planx.uk/src/@planx/components/Checklist/types.ts index cdebd204ad..76ef9d47bd 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/types.ts +++ b/editor.planx.uk/src/@planx/components/Checklist/types.ts @@ -17,14 +17,5 @@ export interface ChecklistProps extends Checklist { } & BaseNodeData; }; } -export interface OptionEditorProps { - index: number; - value: Option; - onChange: (newVal: Option) => void; - groupIndex?: number; - groups?: Array; - onMoveToGroup?: (itemIndex: number, groupIndex: number) => void; - showValueField?: boolean; -} export type Props = PublicProps; diff --git a/editor.planx.uk/src/@planx/components/Question/Editor.tsx b/editor.planx.uk/src/@planx/components/Question/Editor.tsx index e60f851311..bf61ce94ed 100644 --- a/editor.planx.uk/src/@planx/components/Question/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Question/Editor.tsx @@ -10,14 +10,13 @@ import ModalSectionContent from "ui/editor/ModalSectionContent"; import RichTextInput from "ui/editor/RichTextInput/RichTextInput"; import Input from "ui/shared/Input/Input"; import InputRow from "ui/shared/InputRow"; -import InputRowItem from "ui/shared/InputRowItem"; import { Switch } from "ui/shared/Switch"; import { InternalNotes } from "../../../ui/editor/InternalNotes"; import { MoreInformation } from "../../../ui/editor/MoreInformation/MoreInformation"; import { BaseNodeData, Option, parseBaseNodeData } from "../shared"; -import { FlagsSelect } from "../shared/FlagsSelect"; import { ICONS } from "../shared/icons"; +import QuestionOptionsEditor from "./OptionsEditor"; interface Props { node: { @@ -34,100 +33,6 @@ interface Props { handleSubmit?: Function; } -const OptionEditor: React.FC<{ - value: Option; - onChange: (newVal: Option) => void; - showValueField?: boolean; -}> = (props) => ( -
- - {props.value.id && ( - - )} - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - text: ev.target.value, - }, - }); - }} - placeholder="Option" - /> - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - img, - }, - }); - }} - /> - - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - description: ev.target.value, - }, - }); - }} - /> - - {props.showValueField && ( - - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - val: ev.target.value, - }, - }); - }} - /> - - )} - { - props.onChange({ - ...props.value, - data: { - ...props.value.data, - flag: ev, - }, - }); - }} - /> -
-); - export const Question: React.FC = (props) => { const type = TYPES.Question; @@ -246,7 +151,7 @@ export const Question: React.FC = (props) => { }, }) as Option } - Editor={OptionEditor} + Editor={QuestionOptionsEditor} editorExtraProps={{ showValueField: !!formik.values.fn }} /> diff --git a/editor.planx.uk/src/@planx/components/Question/OptionsEditor.tsx b/editor.planx.uk/src/@planx/components/Question/OptionsEditor.tsx new file mode 100644 index 0000000000..d9e0c4792b --- /dev/null +++ b/editor.planx.uk/src/@planx/components/Question/OptionsEditor.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { + BaseOptionsEditor, + BaseOptionsEditorProps, +} from "../shared/BaseOptionsEditor"; + +const QuestionOptionsEditor: React.FC = ({ + value, + onChange, + showValueField = false, +}) => { + return ( + + ); +}; + +export default QuestionOptionsEditor; diff --git a/editor.planx.uk/src/@planx/components/shared/BaseOptionsEditor.tsx b/editor.planx.uk/src/@planx/components/shared/BaseOptionsEditor.tsx new file mode 100644 index 0000000000..702c843868 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/shared/BaseOptionsEditor.tsx @@ -0,0 +1,109 @@ +import React, { ReactNode } from "react"; +import ImgInput from "ui/editor/ImgInput/ImgInput"; +import Input from "ui/shared/Input/Input"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; + +import { Option } from "."; +import { FlagsSelect } from "./FlagsSelect"; + +export interface BaseOptionsEditorProps { + value: Option; + showValueField?: boolean; + showDescriptionField?: boolean; + onChange: (newVal: Option) => void; + children?: ReactNode; +} + +export const BaseOptionsEditor: React.FC = (props) => ( +
+ + {props.value.id && ( + + )} + + { + props.onChange({ + ...props.value, + data: { + ...props.value.data, + text: ev.target.value, + }, + }); + }} + placeholder="Option" + /> + + { + props.onChange({ + ...props.value, + data: { + ...props.value.data, + img, + }, + }); + }} + /> + {props.children} + + {props.showDescriptionField && ( + + + props.onChange({ + ...props.value, + data: { + ...props.value.data, + description: ev.target.value, + }, + }) + } + /> + + )} + {props.showValueField && ( + + { + props.onChange({ + ...props.value, + data: { + ...props.value.data, + val: ev.target.value, + }, + }); + }} + /> + + )} + { + props.onChange({ + ...props.value, + data: { + ...props.value.data, + flag: ev, + }, + }); + }} + /> +
+); From a9b3b2d8926fc80bac5e59eafe223f962cce5e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 12 Dec 2024 11:00:12 +0000 Subject: [PATCH 03/11] chore(api-docs): Add `security` field to `/send-email/{:template}` route (#4073) --- api.planx.uk/modules/sendEmail/docs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api.planx.uk/modules/sendEmail/docs.yaml b/api.planx.uk/modules/sendEmail/docs.yaml index 1aeb4c1ba5..e2a489f61d 100644 --- a/api.planx.uk/modules/sendEmail/docs.yaml +++ b/api.planx.uk/modules/sendEmail/docs.yaml @@ -55,6 +55,8 @@ paths: post: tags: [send email] summary: Send an email + security: + - hasuraAuth: [] parameters: - name: template in: path From 43592f47456ba52545f0ccb0a3929f70f1c427ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 12 Dec 2024 12:55:58 +0000 Subject: [PATCH 04/11] chore(iac): Configure lifecycle policy for AWS ECR (#4074) --- infrastructure/application/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index 96cd773299..f6e978a5cb 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -94,7 +94,17 @@ const CUSTOM_DOMAINS: CustomDomains = export = async () => { const DOMAIN: string = await certificates.requireOutputValue("domain"); - const repo = new awsx.ecr.Repository("repo"); + const repo = new awsx.ecr.Repository("repo", { + lifeCyclePolicyArgs: { + rules: [ + { + description: "Keep last 100 images", + maximumNumberOfImages: 100, + selection: "any", + }, + ], + }, + }); const vpc = awsx.ec2.Vpc.fromExistingIds("vpc", { vpcId: networking.requireOutput("vpcId"), From 891edc13e76ed8afde6d54c05c22ba966f93964f Mon Sep 17 00:00:00 2001 From: Dan G Date: Thu, 12 Dec 2024 15:33:43 +0000 Subject: [PATCH 05/11] boost hasura-proxy Fargate container CPU / memory (staging) (#4076) --- infrastructure/application/Pulumi.production.yaml | 2 ++ infrastructure/application/Pulumi.staging.yaml | 2 ++ infrastructure/application/services/hasura.ts | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/infrastructure/application/Pulumi.production.yaml b/infrastructure/application/Pulumi.production.yaml index 1abd12ce1e..422e043c3d 100644 --- a/infrastructure/application/Pulumi.production.yaml +++ b/infrastructure/application/Pulumi.production.yaml @@ -27,6 +27,8 @@ config: secure: AAABAHfDtVpAD8w32yINWTjgvuRQixWXYFf3/rEcyh59/pRSz+J4ZYCXNq5jqBiIXM2emB+7zOY= application:hasura-cpu: "512" application:hasura-memory: "2048" + application:hasura-proxy-cpu: "512" + application:hasura-proxy-memory: "2048" application:hasura-planx-api-key: secure: AAABAExsXFL7HabeK0Z1oSUJzI2NqVqEmKJ1ojYXyX4Hi8Sbt1Ht9QJc/Yn3cPBAB2r32HKa4HtqqLmfGjS+04lFB/I= application:idox-nexus-client: diff --git a/infrastructure/application/Pulumi.staging.yaml b/infrastructure/application/Pulumi.staging.yaml index 5c34389e81..5463a157ba 100644 --- a/infrastructure/application/Pulumi.staging.yaml +++ b/infrastructure/application/Pulumi.staging.yaml @@ -28,6 +28,8 @@ config: secure: AAABAHsoh7ZNkr6ep3xXsUZpp/JIjshBX+tJ0KOFgGnJ4wxR0oIcB6VewVDuwSyFJRVix72YahM= application:hasura-cpu: "512" application:hasura-memory: "2048" + application:hasura-proxy-cpu: "512" + application:hasura-proxy-memory: "2048" application:hasura-planx-api-key: secure: AAABANHLs3ItPxkteh0chwMP2bKuHO3ovuRLi4FsIrCqerzXVIaTLFDqNR+4KBTeMPz4cnF5tCTwsrJv9GruZdXU+lg= application:idox-nexus-client: diff --git a/infrastructure/application/services/hasura.ts b/infrastructure/application/services/hasura.ts index c27698d872..eef540216d 100644 --- a/infrastructure/application/services/hasura.ts +++ b/infrastructure/application/services/hasura.ts @@ -59,7 +59,8 @@ export const createHasuraService = async ({ containers: { hasuraProxy: { image: repo.buildAndPushImage("../../hasura.planx.uk/proxy"), - memory: 1024 /*MB*/, + cpu: config.requireNumber("hasura-proxy-cpu"), + memory: config.requireNumber("hasura-proxy-memory"), portMappings: [hasuraListenerHttp], environment: [ { name: "HASURA_PROXY_PORT", value: String(HASURA_PROXY_PORT) }, From 70f1372b11f84b1c8580a3d8ab85355e586df67b Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:06:10 +0000 Subject: [PATCH 06/11] chore: setup power automate api keys for Medway (#4078) --- .env.example | 1 + api.planx.uk/.env.test.example | 1 + api.planx.uk/modules/auth/middleware.ts | 4 ++++ docker-compose.yml | 1 + infrastructure/application/Pulumi.production.yaml | 2 ++ infrastructure/application/Pulumi.staging.yaml | 2 ++ infrastructure/application/index.ts | 4 ++++ 7 files changed, 15 insertions(+) diff --git a/.env.example b/.env.example index 766aa593e0..759d790a91 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,7 @@ FILE_API_KEY_BARNET=👻 FILE_API_KEY_LAMBETH=👻 FILE_API_KEY_SOUTHWARK=👻 FILE_API_KEY_EPSOM_EWELL=👻 +FILE_API_KEY_MEDWAY=👻 # Editor EDITOR_URL_EXT=http://localhost:3000 diff --git a/api.planx.uk/.env.test.example b/api.planx.uk/.env.test.example index 4df97c1b50..bdc74b375e 100644 --- a/api.planx.uk/.env.test.example +++ b/api.planx.uk/.env.test.example @@ -23,6 +23,7 @@ FILE_API_KEY_BARNET=👻 FILE_API_KEY_LAMBETH=👻 FILE_API_KEY_SOUTHWARK=👻 FILE_API_KEY_EPSOM_EWELL=👻 +FILE_API_KEY_MEDWAY=👻 # Editor EDITOR_URL_EXT=example.com diff --git a/api.planx.uk/modules/auth/middleware.ts b/api.planx.uk/modules/auth/middleware.ts index d831e9a557..5b4cdfc9d3 100644 --- a/api.planx.uk/modules/auth/middleware.ts +++ b/api.planx.uk/modules/auth/middleware.ts @@ -99,6 +99,10 @@ export const useFilePermission: RequestHandler = (req, _res, next): void => { req.headers["api-key"] as string, process.env.FILE_API_KEY_SOUTHWARK!, ) || + isEqual( + req.headers["api-key"] as string, + process.env.FILE_API_KEY_MEDWAY!, + ) || isEqual( req.headers["api-key"] as string, process.env.FILE_API_KEY_EPSOM_EWELL!, diff --git a/docker-compose.yml b/docker-compose.yml index 34a4c69e8d..b045c55b3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -128,6 +128,7 @@ services: FILE_API_KEY_LAMBETH: ${FILE_API_KEY_LAMBETH} FILE_API_KEY_SOUTHWARK: ${FILE_API_KEY_SOUTHWARK} FILE_API_KEY_EPSOM_EWELL: ${FILE_API_KEY_EPSOM_EWELL} + FILE_API_KEY_MEDWAY: ${FILE_API_KEY_MEDWAY} FILE_API_KEY_NEXUS: ${FILE_API_KEY_NEXUS} FILE_API_KEY: ${FILE_API_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} diff --git a/infrastructure/application/Pulumi.production.yaml b/infrastructure/application/Pulumi.production.yaml index 422e043c3d..6aba6b28c2 100644 --- a/infrastructure/application/Pulumi.production.yaml +++ b/infrastructure/application/Pulumi.production.yaml @@ -14,6 +14,8 @@ config: secure: AAABANvwhiVRBq8NH7ZqcToUzYn4X+KfC5Wm8WjWUKXT5TuVXqC6zHhVVKFBbmdtKjC4j5M4+bWsLiFO9dO0MLobxLpK7YCE application:file-api-key-lambeth: secure: AAABAMNhdCTlFx3fZH/nO71ildypZB2JR5NixlQCENsS1VqwdiOX17q/Gi1UFrCQi2qaY2sZFG4= + application:file-api-key-medway: + secure: AAABABXSnk3j06JukiZ2u/me+pnRjHWAm+rf9FW9bSmzMfr9SWSKyN4XlltOYv1iZKujgI9hGIGiVR40+uYjGjy7BwNqOFxI application:file-api-key-nexus: secure: AAABAB2cv4GAf8RqN1hHbRbO68p8o4kLJYWsip9BoPdobrNtQB787M3s+gJnKKl9DfyXRHOXHGc= application:file-api-key-southwark: diff --git a/infrastructure/application/Pulumi.staging.yaml b/infrastructure/application/Pulumi.staging.yaml index 5463a157ba..49df40ad99 100644 --- a/infrastructure/application/Pulumi.staging.yaml +++ b/infrastructure/application/Pulumi.staging.yaml @@ -15,6 +15,8 @@ config: secure: AAABAD1/nlJ2EOEglLiiNsOLbOd3KWCONhNhJAIdZQVnrSRsNIzX2luszOreQf20EYl8AZ4L1TiheqUHSt22e5z1FiLWoCtY application:file-api-key-lambeth: secure: AAABALQTeIf/uScxASJkhmoPRhewQT94Guad4iJ7GRk0DcND8wDUG0eNxDU4+XwUQZqCnL2DP+E= + application:file-api-key-medway: + secure: AAABABCpSTmTDJU81pG7U57Igr4OtBbX6VuqRooq9Ipzoq1peTenbNQYBWacZn6lyg7ceLAgbiHQgT7LqX2tKN1QwON/BSQh application:file-api-key-nexus: secure: AAABAJFgaBoTWNmZyXDkGRngwU8KpOt6CeBLxGBgBG0JFMsKK7rWT39TsjJ9pL1wZaBoT0YZhCg= application:file-api-key-southwark: diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index f6e978a5cb..a4754a1ab5 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -373,6 +373,10 @@ export = async () => { name: "FILE_API_KEY_EPSOM_EWELL", value: config.requireSecret("file-api-key-epsom-ewell"), }, + { + name: "FILE_API_KEY_MEDWAY", + value: config.requireSecret("file-api-key-medway"), + }, { name: "GOOGLE_CLIENT_ID", value: config.require("google-client-id"), From ea94f96f02c47d65f24ebc81dd713822356d0bc8 Mon Sep 17 00:00:00 2001 From: Ian Jones <51156018+ianjon3s@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:12:38 +0000 Subject: [PATCH 07/11] fix: Selected feedback styling (#4075) --- .../src/@planx/components/Feedback/components/FaceBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor.planx.uk/src/@planx/components/Feedback/components/FaceBox.tsx b/editor.planx.uk/src/@planx/components/Feedback/components/FaceBox.tsx index 32f50cfd86..12a89180e2 100644 --- a/editor.planx.uk/src/@planx/components/Feedback/components/FaceBox.tsx +++ b/editor.planx.uk/src/@planx/components/Feedback/components/FaceBox.tsx @@ -28,7 +28,7 @@ export const FaceBox = ({ px: 0, width: "100%", textTransform: "none", - [`&.${toggleButtonClasses.selected}`]: { + [`&.${toggleButtonClasses.selected} > div`]: { borderColor: (theme) => theme.palette.primary.dark, background: (theme) => theme.palette.background.paper, }, From 47aba73d1eefdf8782b12493264a925e1a58969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 13 Dec 2024 12:01:34 +0000 Subject: [PATCH 08/11] fix(editor): Do not attempt to create flow after throwing alert (#4079) --- editor.planx.uk/src/pages/Team.tsx | 70 +++++++++++++----------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/editor.planx.uk/src/pages/Team.tsx b/editor.planx.uk/src/pages/Team.tsx index ad0c8b9d0b..37c10d6d37 100644 --- a/editor.planx.uk/src/pages/Team.tsx +++ b/editor.planx.uk/src/pages/Team.tsx @@ -258,59 +258,57 @@ const FlowItem: React.FC = ({ }; const GetStarted: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => ( - ({ - mt: 4, - backgroundColor: theme.palette.background.paper, - borderRadius: "8px", - display: "flex", - flexDirection: "column", + ({ + mt: 4, + backgroundColor: theme.palette.background.paper, + borderRadius: "8px", + display: "flex", + flexDirection: "column", alignItems: "center", - gap: 2, - padding: 2 - })}> - No services found - Get started by creating your first service - + gap: 2, + padding: 2, + })} + > + No services found + Get started by creating your first service + -) +); const AddFlowButton: React.FC<{ flows: FlowSummary[] }> = ({ flows }) => { const { navigate } = useNavigation(); - const { teamId, createFlow, teamSlug } = useStore() + const { teamId, createFlow, teamSlug } = useStore(); const addFlow = async () => { const newFlowName = prompt("Service name"); if (!newFlowName) return; const newFlowSlug = slugify(newFlowName); - const duplicateFlowName = flows?.find( - (flow) => flow.slug === newFlowSlug, - ); + const duplicateFlowName = flows?.find((flow) => flow.slug === newFlowSlug); if (duplicateFlowName) { alert( `The flow "${newFlowName}" already exists. Enter a unique flow name to continue`, ); + return; } const newId = await createFlow(teamId, newFlowSlug, newFlowName); navigate(`/${teamSlug}/${newId}`); - } + }; - return( - - Add a new service - - ) -} + return Add a new service; +}; const Team: React.FC = () => { - const [{ id: teamId, slug }, canUserEditTeam, getFlows] = useStore((state) => [state.getTeam(), state.canUserEditTeam, state.getFlows ]); + const [{ id: teamId, slug }, canUserEditTeam, getFlows] = useStore( + (state) => [state.getTeam(), state.canUserEditTeam, state.getFlows], + ); const [flows, setFlows] = useState(null); const fetchFlows = useCallback(() => { - getFlows(teamId) - .then((flows) => { + getFlows(teamId).then((flows) => { // Copy the array and sort by most recently edited desc using last associated operation.createdAt, not flow.updatedAt const sortedFlows = flows.toSorted((a, b) => b.operations[0]["createdAt"].localeCompare( @@ -325,7 +323,7 @@ const Team: React.FC = () => { fetchFlows(); }, [fetchFlows]); - const teamHasFlows = flows && Boolean(flows.length) + const teamHasFlows = flows && Boolean(flows.length); const showAddFlowButton = teamHasFlows && canUserEditTeam(slug); return ( @@ -349,15 +347,9 @@ const Team: React.FC = () => { Services - {canUserEditTeam(slug) ? ( - - ) : ( - - )} + {canUserEditTeam(slug) ? : } - {showAddFlowButton && ( - - )} + {showAddFlowButton && } {teamHasFlows && ( @@ -373,9 +365,9 @@ const Team: React.FC = () => { }} /> ))} - ) - } - { flows && !flows.length && } + + )} + {flows && !flows.length && } ); }; From 3c0358fbc5e2a97bd8024823e5aa3cd986f8f583 Mon Sep 17 00:00:00 2001 From: Dan G Date: Fri, 13 Dec 2024 16:24:26 +0000 Subject: [PATCH 09/11] extend timeout for Hasura migrations at boot (#4080) --- .gitignore | 2 ++ infrastructure/application/services/hasura.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 147f72b877..b95f376eae 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ hasura.planx.uk/.env.test /playwright-report/ /playwright/.cache/ api.planx.uk/tmp/ +.python-version +__pycache__ # Ignore certificate files **/*.chain diff --git a/infrastructure/application/services/hasura.ts b/infrastructure/application/services/hasura.ts index eef540216d..c4b3f63028 100644 --- a/infrastructure/application/services/hasura.ts +++ b/infrastructure/application/services/hasura.ts @@ -98,6 +98,7 @@ export const createHasuraService = async ({ name: "HASURA_GRAPHQL_DATABASE_URL", value: dbRootUrl, }, + { name: "HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT", value: "300" }, { name: "HASURA_PLANX_API_URL", value: `https://api.${DOMAIN}`, From 8607b975f03ae857ee1085cdabd20a3c676a115d Mon Sep 17 00:00:00 2001 From: Dan G Date: Fri, 13 Dec 2024 20:50:33 +0000 Subject: [PATCH 10/11] bump staging RDS instance to T3 Small (#4081) --- infrastructure/data/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/data/index.ts b/infrastructure/data/index.ts index 78fe9270bd..7046441910 100644 --- a/infrastructure/data/index.ts +++ b/infrastructure/data/index.ts @@ -16,7 +16,7 @@ const db = new aws.rds.Instance("app", { // $ aws rds describe-db-engine-versions --default-only --engine postgres engineVersion: "12.17", // Available instance types: https://aws.amazon.com/rds/instance-types/ - instanceClass: env === "production" ? "db.t3.medium" : "db.t3.micro", + instanceClass: env === "production" ? "db.t3.medium" : "db.t3.small", allocatedStorage: env === "production" ? 100 : 20, allowMajorVersionUpgrade: true, dbSubnetGroupName: networking.requireOutput("subnetId"), From d87637e859b520cd005b108a427fa1ac4d86e8b7 Mon Sep 17 00:00:00 2001 From: augustlindemer <118665588+augustlindemer@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:18:19 +0000 Subject: [PATCH 11/11] feat: Community Infrastructure Levy (CIL) schemas (#4084) --- .../src/@planx/components/List/Editor.tsx | 6 ++ .../List/schemas/CIL/ExistingCIL.ts | 65 +++++++++++++++++++ .../List/schemas/CIL/MezzanineCIL.ts | 27 ++++++++ .../List/schemas/CIL/UnoccupiedCIL.ts | 44 +++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 editor.planx.uk/src/@planx/components/List/schemas/CIL/ExistingCIL.ts create mode 100644 editor.planx.uk/src/@planx/components/List/schemas/CIL/MezzanineCIL.ts create mode 100644 editor.planx.uk/src/@planx/components/List/schemas/CIL/UnoccupiedCIL.ts diff --git a/editor.planx.uk/src/@planx/components/List/Editor.tsx b/editor.planx.uk/src/@planx/components/List/Editor.tsx index 72b01dc3b7..339c08ca9c 100644 --- a/editor.planx.uk/src/@planx/components/List/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/List/Editor.tsx @@ -17,6 +17,9 @@ import { ICONS } from "../shared/icons"; import { EditorProps } from "../shared/types"; import { List, parseContent, validationSchema } from "./model"; import { ProposedAdvertisements } from "./schemas/Adverts"; +import { ExistingBuildingsCIL } from "./schemas/CIL/ExistingCIL"; +import { MezzanineCIL } from "./schemas/CIL/MezzanineCIL"; +import { UnoccupiedBuildingsCIL } from "./schemas/CIL/UnoccupiedCIL"; import { NonResidentialFloorspace } from "./schemas/Floorspace"; import { BuildingDetailsGLA } from "./schemas/GLA/BuildingDetails"; import { CommunalSpaceGLA } from "./schemas/GLA/CommunalSpace"; @@ -69,6 +72,9 @@ export const SCHEMAS = [ { name: "Proposed advertisements", schema: ProposedAdvertisements }, { name: "Parking details", schema: Parking }, { name: "Parking details (GLA)", schema: ParkingGLA }, + { name: "Existing buildings (CIL)", schema: ExistingBuildingsCIL }, + { name: "Unoccupied buildings (CIL)", schema: UnoccupiedBuildingsCIL }, + { name: "Mezzanine floors (CIL)", schema: MezzanineCIL }, { name: "Trees", schema: Trees }, { name: "Trees (Map first)", schema: TreesMapFirst }, ]; diff --git a/editor.planx.uk/src/@planx/components/List/schemas/CIL/ExistingCIL.ts b/editor.planx.uk/src/@planx/components/List/schemas/CIL/ExistingCIL.ts new file mode 100644 index 0000000000..3f69e83302 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/schemas/CIL/ExistingCIL.ts @@ -0,0 +1,65 @@ +import { Schema } from "@planx/components/shared/Schema/model"; +import { TextInputType } from "@planx/components/TextInput/model"; + +export const ExistingBuildingsCIL: Schema = { + type: "Existing building or part of building", + fields: [ + { + type: "text", + data: { + title: "Describe the existing building or part", + fn: "descriptionExisting", + type: TextInputType.Short, + }, + }, + { + type: "number", + data: { + title: "How much of its floorspace will be retained?", + units: "m²", + fn: "area.retained", + allowNegatives: false, + }, + }, + { + type: "text", + data: { + title: "What will the retained floorspace be used for?", + description: "This can be identical to its current use.", + fn: "descriptionProposed", + type: TextInputType.Short, + }, + }, + { + type: "number", + data: { + title: "How much of its floorspace will be lost?", + units: "m²", + fn: "area.loss", + allowNegatives: false, + }, + }, + { + type: "question", + data: { + title: + "Has the building or part been lawfully occupied for 6 continuous months in the past 36 months?", + fn: "continuousOccupation", + options: [ + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, + ], + }, + }, + { + type: "text", + data: { + title: "When was it last occupied for its lawful use?", + description: "Please enter a date or whether it is still in use.", + fn: "lastOccupation", + type: TextInputType.Short, + }, + }, + ], + min: 1, +} as const; diff --git a/editor.planx.uk/src/@planx/components/List/schemas/CIL/MezzanineCIL.ts b/editor.planx.uk/src/@planx/components/List/schemas/CIL/MezzanineCIL.ts new file mode 100644 index 0000000000..3c216892d6 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/schemas/CIL/MezzanineCIL.ts @@ -0,0 +1,27 @@ +import { Schema } from "@planx/components/shared/Schema/model"; +import { TextInputType } from "@planx/components/TextInput/model"; + +export const MezzanineCIL: Schema = { + type: "New mezzanine floor", + fields: [ + { + type: "text", + data: { + title: "Describe the use of the mezzanine", + fn: "description", + type: TextInputType.Short, + }, + }, + { + type: "number", + data: { + title: + "What will be the Gross Internal Floor Area (GIA) of the mezzanine?", + units: "m²", + fn: "area", + allowNegatives: false, + }, + }, + ], + min: 1, +} as const; diff --git a/editor.planx.uk/src/@planx/components/List/schemas/CIL/UnoccupiedCIL.ts b/editor.planx.uk/src/@planx/components/List/schemas/CIL/UnoccupiedCIL.ts new file mode 100644 index 0000000000..83126000b9 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/schemas/CIL/UnoccupiedCIL.ts @@ -0,0 +1,44 @@ +import { Schema } from "@planx/components/shared/Schema/model"; +import { TextInputType } from "@planx/components/TextInput/model"; + +export const UnoccupiedBuildingsCIL: Schema = { + type: "Building not meant for occupation or temporarily permitted", + fields: [ + { + type: "text", + data: { + title: "Describe the existing building", + fn: "descriptionExisting", + type: TextInputType.Short, + }, + }, + { + type: "number", + data: { + title: "How much of its floorspace will be retained?", + units: "m²", + fn: "area.retained", + allowNegatives: false, + }, + }, + { + type: "text", + data: { + title: "What will the retained floorspace be used for?", + description: "This can be identical to its current use.", + fn: "descriptionProposed", + type: TextInputType.Short, + }, + }, + { + type: "number", + data: { + title: "How much of its floorspace will be lost?", + units: "m²", + fn: "area.loss", + allowNegatives: false, + }, + }, + ], + min: 1, +} as const;