diff --git a/.github/workflows/pizza-teardown.yml b/.github/workflows/pizza-teardown.yml index 5eec564860..24e3de0de2 100644 --- a/.github/workflows/pizza-teardown.yml +++ b/.github/workflows/pizza-teardown.yml @@ -17,7 +17,7 @@ jobs: action: destroy api_key: ${{ secrets.VULTR_API_KEY }} domain: ${{ env.DOMAIN }} - os_type: alpine + os_type: ubuntu plan: vc2-1c-1gb pull_request_id: ${{ env.PULLREQUEST_ID }} region: lhr diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f61e29c664..870fb1b4c8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -172,7 +172,7 @@ jobs: working-directory: ${{ env.EDITOR_DIRECTORY }} - run: pnpm build working-directory: ${{ env.EDITOR_DIRECTORY }} - - run: pnpm test + - run: pnpm test:silent working-directory: ${{ env.EDITOR_DIRECTORY }} build_react_app: @@ -307,7 +307,7 @@ jobs: action: create api_key: ${{ secrets.VULTR_API_KEY }} domain: ${{ env.DOMAIN }} - os_type: alpine + os_type: ubuntu plan: vc2-1c-1gb pull_request_id: ${{ env.PULLREQUEST_ID }} region: lhr @@ -324,19 +324,13 @@ jobs: password: ${{ steps.create.outputs.default_password }} command_timeout: 20m script: | - apk update - apk add docker - addgroup root docker - rc-update add docker default - service docker start - apk add docker-cli-compose - - apk add git + apt-get update -y + git clone "${{ secrets.AUTHENTICATED_REPO_URL }}" cd planx-new git fetch origin "pull/${{ env.PULLREQUEST_ID }}/head" && git checkout FETCH_HEAD - apk add aws-cli + apt-get install awscli -y export AWS_ACCESS_KEY_ID=${{ secrets.PIZZA_AWS_ACCESS_KEY_ID }} export AWS_SECRET_ACCESS_KEY=${{ secrets.PIZZA_AWS_SECRET_ACCESS_KEY }} export AWS_REGION=eu-west-2 @@ -358,21 +352,15 @@ jobs: username: root password: ${{ secrets.SSH_PASSWORD }} command_timeout: 10m - # TODO: some of below script might be superfluous for server update (rather than create) script: | - apk update - apk add docker - addgroup root docker - rc-update add docker default - service docker start - apk add docker-cli-compose + apt-get update -y git clone "${{ secrets.AUTHENTICATED_REPO_URL }}" cd planx-new git add . && git stash git fetch origin "pull/${{ env.PULLREQUEST_ID }}/head" && git checkout FETCH_HEAD - apk add aws-cli + apt-get install awscli -y export AWS_ACCESS_KEY_ID=${{ secrets.PIZZA_AWS_ACCESS_KEY_ID }} export AWS_SECRET_ACCESS_KEY=${{ secrets.PIZZA_AWS_SECRET_ACCESS_KEY }} export AWS_REGION=eu-west-2 diff --git a/.github/workflows/regression-tests.yml b/.github/workflows/regression-tests.yml index 7408cf97b2..6606f07e5c 100644 --- a/.github/workflows/regression-tests.yml +++ b/.github/workflows/regression-tests.yml @@ -119,7 +119,7 @@ jobs: working-directory: ${{ env.EDITOR_DIRECTORY }} - run: pnpm build working-directory: ${{ env.EDITOR_DIRECTORY }} - - run: pnpm test + - run: pnpm test:silent working-directory: ${{ env.EDITOR_DIRECTORY }} end_to_end_tests: diff --git a/api.planx.uk/modules/gis/service/digitalLand.ts b/api.planx.uk/modules/gis/service/digitalLand.ts index 9298eefd31..6f8d968cef 100644 --- a/api.planx.uk/modules/gis/service/digitalLand.ts +++ b/api.planx.uk/modules/gis/service/digitalLand.ts @@ -5,7 +5,7 @@ import type { } from "@opensystemslab/planx-core/types"; import { gql } from "graphql-request"; import fetch from "isomorphic-fetch"; -import { addDesignatedVariable, omitGeometry } from "./helpers"; +import { addDesignatedVariable } from "./helpers"; import { baseSchema } from "./local_authorities/metadata/base"; import { $api } from "../../../client"; @@ -119,14 +119,14 @@ async function go( ); // because there can be many digital land datasets per planx variable, check if this key is already in our result if (key && Object.keys(formattedResult).includes(key)) { - formattedResult[key]["data"]?.push(omitGeometry(entity)); + formattedResult[key]["data"]?.push(entity); } else { if (key) { formattedResult[key] = { fn: key, value: true, text: baseSchema[key].pos, - data: [omitGeometry(entity)], + data: [entity], category: baseSchema[key].category, }; } diff --git a/api.planx.uk/modules/pay/controller.ts b/api.planx.uk/modules/pay/controller.ts index 4b2c0266b0..0f8a304335 100644 --- a/api.planx.uk/modules/pay/controller.ts +++ b/api.planx.uk/modules/pay/controller.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { Request } from "express"; import { responseInterceptor } from "http-proxy-middleware"; -import { logPaymentStatus } from "../send/utils/helpers"; +import { logPaymentStatus } from "./helpers"; import { usePayProxy } from "./proxy"; import { $api } from "../../client"; import { ServerError } from "../../errors"; diff --git a/api.planx.uk/modules/pay/helpers.ts b/api.planx.uk/modules/pay/helpers.ts new file mode 100644 index 0000000000..ee8826cae7 --- /dev/null +++ b/api.planx.uk/modules/pay/helpers.ts @@ -0,0 +1,112 @@ +import { gql } from "graphql-request"; +import airbrake from "../../airbrake"; +import { $api } from "../../client"; + +export async function logPaymentStatus({ + sessionId, + flowId, + teamSlug, + govUkResponse, +}: { + sessionId: string | undefined; + flowId: string | undefined; + teamSlug: string; + govUkResponse: { + amount: number; + payment_id: string; + state: { + status: string; + }; + }; +}): Promise { + if (!flowId || !sessionId) { + reportError({ + error: "Could not log the payment status due to missing context value(s)", + context: { sessionId, flowId, teamSlug }, + }); + } else { + // log payment status response + try { + await insertPaymentStatus({ + sessionId, + flowId, + teamSlug, + paymentId: govUkResponse.payment_id, + status: govUkResponse.state?.status || "unknown", + amount: govUkResponse.amount, + }); + } catch (e) { + reportError({ + error: `Failed to insert a payment status: ${e}`, + context: { govUkResponse }, + }); + } + } +} + +// TODO: this would ideally live in planx-client +async function insertPaymentStatus({ + flowId, + sessionId, + paymentId, + teamSlug, + status, + amount, +}: { + flowId: string; + sessionId: string; + paymentId: string; + teamSlug: string; + status: string; + amount: number; +}): Promise { + const _response = await $api.client.request( + gql` + mutation InsertPaymentStatus( + $flowId: uuid! + $sessionId: uuid! + $paymentId: String! + $teamSlug: String! + $status: payment_status_enum_enum + $amount: Int! + ) { + insert_payment_status( + objects: { + flow_id: $flowId + session_id: $sessionId + payment_id: $paymentId + team_slug: $teamSlug + status: $status + amount: $amount + } + ) { + affected_rows + } + } + `, + { + flowId, + sessionId, + teamSlug, + paymentId, + status, + amount, + }, + ); +} + +// tmp explicit error handling +export function reportError(report: { error: any; context: object }) { + if (airbrake) { + airbrake.notify(report); + return; + } + log(report); +} + +// tmp logger +function log(event: object | string) { + if (!process.env.SUPPRESS_LOGS) { + console.log(event); + } +} diff --git a/api.planx.uk/modules/send/s3/index.ts b/api.planx.uk/modules/send/s3/index.ts index 350fc43629..f301729334 100644 --- a/api.planx.uk/modules/send/s3/index.ts +++ b/api.planx.uk/modules/send/s3/index.ts @@ -3,6 +3,8 @@ import { $api } from "../../../client"; import { uploadPrivateFile } from "../../file/service/uploadFile"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils"; import axios from "axios"; +import { isApplicationTypeSupported } from "../utils/helpers"; +import { Passport } from "../../../types"; export async function sendToS3( req: Request, @@ -40,8 +42,14 @@ export async function sendToS3( }); } - // Generate the ODP Schema JSON - const exportData = await $api.export.digitalPlanningDataPayload(sessionId); + const session = await $api.session.find(sessionId); + const passport = session?.data?.passport as Passport; + + // Generate the ODP Schema JSON, skipping validation if not a supported application type + const doValidation = isApplicationTypeSupported(passport); + const exportData = doValidation + ? await $api.export.digitalPlanningDataPayload(sessionId) + : await $api.export.digitalPlanningDataPayload(sessionId, true); // Create and upload the data as an S3 file const { fileUrl } = await uploadPrivateFile( @@ -63,6 +71,7 @@ export async function sendToS3( message: "New submission from PlanX", environment: env, file: fileUrl, + payload: doValidation ? "Validated ODP Schema" : "Discretionary", }, }) .then((res) => { @@ -80,6 +89,7 @@ export async function sendToS3( return res.status(200).send({ message: `Successfully uploaded submission to S3: ${fileUrl}`, + payload: doValidation ? "Validated ODP Schema" : "Discretionary", webhookResponse: webhookResponseStatus, }); } catch (error) { diff --git a/api.planx.uk/modules/send/utils/exportZip.test.ts b/api.planx.uk/modules/send/utils/exportZip.test.ts index f0bde23113..ea9798d314 100644 --- a/api.planx.uk/modules/send/utils/exportZip.test.ts +++ b/api.planx.uk/modules/send/utils/exportZip.test.ts @@ -232,7 +232,7 @@ describe("buildSubmissionExportZip", () => { ); }); - test("ODP schema json is excluded if unsupported application type", async () => { + test("ODP schema json is included, but not validated, if unsupported application type", async () => { // set-up mock session passport overwriting "application.type" const lowcalSessionUnsupportedAppType: Partial = { ...mockLowcalSession, @@ -243,7 +243,7 @@ describe("buildSubmissionExportZip", () => { passport: { data: { ...mockLowcalSession.data!.passport.data, - "application.type": ["listedBuildingConsent"], + "application.type": ["reportAPlanningBreach"], }, }, }, @@ -255,13 +255,13 @@ describe("buildSubmissionExportZip", () => { includeDigitalPlanningJSON: true, }); - expect(mockAddFile).not.toHaveBeenCalledWith( + expect(mockAddFile).toHaveBeenCalledWith( "application.json", expect.anything(), ); }); - test("ODP schema json is excluded if no application type", async () => { + test("ODP schema json is included, but not validated, if no application type", async () => { // set-up mock session passport overwriting "application.type" const lowcalSessionUnsupportedAppType: Partial = { ...mockLowcalSession, @@ -284,7 +284,7 @@ describe("buildSubmissionExportZip", () => { includeDigitalPlanningJSON: true, }); - expect(mockAddFile).not.toHaveBeenCalledWith( + expect(mockAddFile).toHaveBeenCalledWith( "application.json", expect.anything(), ); diff --git a/api.planx.uk/modules/send/utils/exportZip.ts b/api.planx.uk/modules/send/utils/exportZip.ts index 76fffed299..2d15f79c2f 100644 --- a/api.planx.uk/modules/send/utils/exportZip.ts +++ b/api.planx.uk/modules/send/utils/exportZip.ts @@ -16,6 +16,7 @@ import { Passport } from "@opensystemslab/planx-core"; import type { Passport as IPassport } from "../../../types"; import type { Stream } from "node:stream"; import type { PlanXExportData } from "@opensystemslab/planx-core/types"; +import { isApplicationTypeSupported } from "./helpers"; export async function buildSubmissionExportZip({ sessionId, @@ -30,7 +31,7 @@ export async function buildSubmissionExportZip({ const sessionData = await $api.session.find(sessionId); if (!sessionData) { throw new Error( - `session ${sessionId} not found so could not create Uniform submission zip`, + `session ${sessionId} not found so could not create submission zip`, ); } const passport = sessionData.data?.passport as IPassport; @@ -50,21 +51,18 @@ export async function buildSubmissionExportZip({ }); } catch (error) { throw new Error( - `Failed to generate OneApp XML for ${sessionId}. Error - ${error}`, + `Failed to generate OneApp XML for ${sessionId} zip. Error - ${error}`, ); } } - // add ODP Schema JSON to the zip for supported application types - const supportedApplicationPrefixes = ["ldc", "pa", "pp"]; - const applicationType = passport.data?.["application.type"]?.[0]; - if ( - includeDigitalPlanningJSON && - applicationType && - supportedApplicationPrefixes.includes(applicationType.split(".")?.[0]) - ) { + // add ODP Schema JSON to the zip, skipping validation if an unsupported application type + if (includeDigitalPlanningJSON) { try { - const schema = await $api.export.digitalPlanningDataPayload(sessionId); + const doValidation = isApplicationTypeSupported(passport); + const schema = doValidation + ? await $api.export.digitalPlanningDataPayload(sessionId) + : await $api.export.digitalPlanningDataPayload(sessionId, true); const schemaBuff = Buffer.from(JSON.stringify(schema, null, 2)); zip.addFile({ name: "application.json", diff --git a/api.planx.uk/modules/send/utils/helpers.ts b/api.planx.uk/modules/send/utils/helpers.ts index 06db45b327..dda80de7f8 100644 --- a/api.planx.uk/modules/send/utils/helpers.ts +++ b/api.planx.uk/modules/send/utils/helpers.ts @@ -1,112 +1,17 @@ -import { gql } from "graphql-request"; -import airbrake from "../../../airbrake"; -import { $api } from "../../../client"; +import { Passport } from "../../../types"; -export async function logPaymentStatus({ - sessionId, - flowId, - teamSlug, - govUkResponse, -}: { - sessionId: string | undefined; - flowId: string | undefined; - teamSlug: string; - govUkResponse: { - amount: number; - payment_id: string; - state: { - status: string; - }; - }; -}): Promise { - if (!flowId || !sessionId) { - reportError({ - error: "Could not log the payment status due to missing context value(s)", - context: { sessionId, flowId, teamSlug }, - }); - } else { - // log payment status response - try { - await insertPaymentStatus({ - sessionId, - flowId, - teamSlug, - paymentId: govUkResponse.payment_id, - status: govUkResponse.state?.status || "unknown", - amount: govUkResponse.amount, - }); - } catch (e) { - reportError({ - error: `Failed to insert a payment status: ${e}`, - context: { govUkResponse }, - }); - } - } -} - -// tmp explicit error handling -export function reportError(report: { error: any; context: object }) { - if (airbrake) { - airbrake.notify(report); - return; - } - log(report); -} +/** + * Checks whether a session's passport data includes an application type supported by the ODP Schema + * @param passport + * @returns boolean + */ +export function isApplicationTypeSupported(passport: Passport): boolean { + // Prefixes of application types that are supported by the ODP Schema + // TODO in future look up via schema type definitions + const supportedAppTypes = ["ldc", "listed", "pa", "pp"]; -// tmp logger -function log(event: object | string) { - if (!process.env.SUPPRESS_LOGS) { - console.log(event); - } -} + const appType = passport.data?.["application.type"]?.[0]; + const appTypePrefix = appType?.split(".")?.[0]; -// TODO: this would ideally live in planx-client -async function insertPaymentStatus({ - flowId, - sessionId, - paymentId, - teamSlug, - status, - amount, -}: { - flowId: string; - sessionId: string; - paymentId: string; - teamSlug: string; - status: string; - amount: number; -}): Promise { - const _response = await $api.client.request( - gql` - mutation InsertPaymentStatus( - $flowId: uuid! - $sessionId: uuid! - $paymentId: String! - $teamSlug: String! - $status: payment_status_enum_enum - $amount: Int! - ) { - insert_payment_status( - objects: { - flow_id: $flowId - session_id: $sessionId - payment_id: $paymentId - team_slug: $teamSlug - status: $status - amount: $amount - } - ) { - affected_rows - } - } - `, - { - flowId, - sessionId, - teamSlug, - paymentId, - status, - amount, - }, - ); + return supportedAppTypes.includes(appTypePrefix); } diff --git a/api.planx.uk/modules/webhooks/service/validateInput/utils.ts b/api.planx.uk/modules/webhooks/service/validateInput/utils.ts index 3f8b772598..0eb4c0a0b2 100644 --- a/api.planx.uk/modules/webhooks/service/validateInput/utils.ts +++ b/api.planx.uk/modules/webhooks/service/validateInput/utils.ts @@ -1,8 +1,8 @@ import { isObject } from "lodash"; import { JSDOM } from "jsdom"; import createDOMPurify from "dompurify"; -import { reportError } from "../../../send/utils/helpers"; import { decode } from "he"; +import { reportError } from "../../../pay/helpers"; // Setup JSDOM and DOMPurify const window = new JSDOM("").window; diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 802b50c1b7..3e05e74575 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -11,6 +11,8 @@ services: restart: always volumes: - "./api.planx.uk:/api" + - "/api/node_modules" + environment: NODE_ENV: "development" diff --git a/editor.planx.uk/.husky/pre-commit b/editor.planx.uk/.husky/pre-commit index 1040d3911d..a99f524c02 100755 --- a/editor.planx.uk/.husky/pre-commit +++ b/editor.planx.uk/.husky/pre-commit @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/bin/sh . "$(dirname -- "$0")/_/husky.sh" cd editor.planx.uk npx lint-staged diff --git a/editor.planx.uk/.husky/pre-push b/editor.planx.uk/.husky/pre-push index e19b53211f..7e8796be4a 100755 --- a/editor.planx.uk/.husky/pre-push +++ b/editor.planx.uk/.husky/pre-push @@ -1,3 +1,4 @@ +#!/bin/sh echo "Checking for git author…" git log -1 --pretty=format:%ae origin/main...HEAD | grep --silent "harles" && echo "Missing running command: gauthor" && exit 1 git log -1 --pretty=format:%an origin/main...HEAD | grep --silent "harles" && echo "Missing running command: gauthor" && exit 1 diff --git a/editor.planx.uk/src/@planx/components/DateInput/DateInput.stories.tsx b/editor.planx.uk/src/@planx/components/DateInput/DateInput.stories.tsx index 667b758699..316b7c7b64 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/DateInput.stories.tsx +++ b/editor.planx.uk/src/@planx/components/DateInput/DateInput.stories.tsx @@ -32,17 +32,17 @@ export const FilledForm: StoryObj = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const dayInput = canvas.getByPlaceholderText("DD"); + const dayInput = canvas.getByLabelText("Day"); await userEvent.type(dayInput, "01", { delay: 100, }); - const monthInput = canvas.getByPlaceholderText("MM"); + const monthInput = canvas.getByLabelText("Month"); await userEvent.type(monthInput, "03", { delay: 100, }); - const yearInput = canvas.getByPlaceholderText("YYYY"); + const yearInput = canvas.getByLabelText("Year"); await userEvent.type(yearInput, "2030", { delay: 100, }); diff --git a/editor.planx.uk/src/@planx/components/DateInput/Editor.test.tsx b/editor.planx.uk/src/@planx/components/DateInput/Editor.test.tsx index 4b8ac996cf..5a866e2e82 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/Editor.test.tsx +++ b/editor.planx.uk/src/@planx/components/DateInput/Editor.test.tsx @@ -29,9 +29,11 @@ describe("DateInputComponent - Editor Modal", () => { , ); - const [minDay, maxDay] = screen.getAllByPlaceholderText("DD"); - const [minMonth, maxMonth] = screen.getAllByPlaceholderText("MM"); - const [minYear, maxYear] = screen.getAllByPlaceholderText("YYYY"); + const [minDay, maxDay] = screen.getAllByRole("textbox", { name: "Day" }); + const [minMonth, maxMonth] = screen.getAllByRole("textbox", { + name: "Month", + }); + const [minYear, maxYear] = screen.getAllByRole("textbox", { name: "Year" }); expect(screen.queryByText(minError)).toBeNull(); expect(screen.queryByText(maxError)).toBeNull(); @@ -59,9 +61,9 @@ describe("DateInputComponent - Editor Modal", () => { , ); - const [minDay, maxDay] = screen.getAllByPlaceholderText("DD"); - const [minMonth, maxMonth] = screen.getAllByPlaceholderText("MM"); - const [minYear, maxYear] = screen.getAllByPlaceholderText("YYYY"); + const [minDay, maxDay] = screen.getAllByLabelText("Day"); + const [minMonth, maxMonth] = screen.getAllByLabelText("Month"); + const [minYear, maxYear] = screen.getAllByLabelText("Year"); expect(screen.queryByText(minError)).toBeNull(); expect(screen.queryByText(maxError)).toBeNull(); @@ -91,9 +93,9 @@ describe("DateInputComponent - Editor Modal", () => { , ); - const [minDay, maxDay] = screen.getAllByPlaceholderText("DD"); - const [minMonth, maxMonth] = screen.getAllByPlaceholderText("MM"); - const [minYear, maxYear] = screen.getAllByPlaceholderText("YYYY"); + const [minDay, maxDay] = screen.getAllByLabelText("Day"); + const [minMonth, maxMonth] = screen.getAllByLabelText("Month"); + const [minYear, maxYear] = screen.getAllByLabelText("Year"); expect(screen.queryAllByText(invalidError)).toHaveLength(0); diff --git a/editor.planx.uk/src/@planx/components/DateInput/Editor.tsx b/editor.planx.uk/src/@planx/components/DateInput/Editor.tsx index a0db680f5f..69f1aff7d0 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/DateInput/Editor.tsx @@ -72,6 +72,7 @@ const DateInputComponent: React.FC = (props) => { = (props) => { { @@ -21,10 +21,10 @@ test("submits a date", async () => { expect(screen.getByRole("heading")).toHaveTextContent("Pizza Day"); - await fillInFieldsUsingPlaceholder(user, { - DD: "22", - MM: "05", - YYYY: "2010", + await fillInFieldsUsingLabel(user, { + Day: "22", + Month: "05", + Year: "2010", }); await user.click(screen.getByTestId("continue-button")); @@ -103,7 +103,7 @@ test("allows user to type into input field and click continue", async () => { , ); - const day = screen.getByPlaceholderText("DD"); + const day = screen.getByLabelText("Day"); await user.type(day, "2"); // Trigger blur event @@ -111,12 +111,12 @@ test("allows user to type into input field and click continue", async () => { expect(day).toHaveValue("02"); - const month = screen.getByPlaceholderText("MM"); + const month = screen.getByLabelText("Month"); await user.type(month, "1"); await user.type(month, "1"); expect(month).toHaveValue("11"); - const year = screen.getByPlaceholderText("YYYY"); + const year = screen.getByLabelText("Year"); await user.type(year, "1"); await user.type(year, "9"); await user.type(year, "9"); @@ -129,9 +129,9 @@ test("allows user to type into input field and click continue", async () => { test("date fields have a max length set", async () => { setup(); - const day = screen.getByPlaceholderText("DD") as HTMLInputElement; - const month = screen.getByPlaceholderText("MM") as HTMLInputElement; - const year = screen.getByPlaceholderText("YYYY") as HTMLInputElement; + const day = screen.getByLabelText("Day") as HTMLInputElement; + const month = screen.getByLabelText("Month") as HTMLInputElement; + const year = screen.getByLabelText("Year") as HTMLInputElement; expect(day.maxLength).toBe(2); expect(month.maxLength).toBe(2); diff --git a/editor.planx.uk/src/@planx/components/List/Editor.tsx b/editor.planx.uk/src/@planx/components/List/Editor.tsx index 080e07a605..d2975e52ed 100644 --- a/editor.planx.uk/src/@planx/components/List/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/List/Editor.tsx @@ -1,13 +1,28 @@ +import MenuItem from "@mui/material/MenuItem"; import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import { useFormik } from "formik"; import React from "react"; -import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder"; +import ModalSection from "ui/editor/ModalSection"; +import ModalSectionContent from "ui/editor/ModalSectionContent"; +import RichTextInput from "ui/editor/RichTextInput"; +import SelectInput from "ui/editor/SelectInput"; +import Input from "ui/shared/Input"; +import InputRow from "ui/shared/InputRow"; +import InputRowItem from "ui/shared/InputRowItem"; +import InputRowLabel from "ui/shared/InputRowLabel"; -import { EditorProps } from "../ui"; +import { EditorProps, ICONS, InternalNotes, MoreInformation } from "../ui"; import { List, parseContent } from "./model"; +import { Zoo } from "./schemas/Zoo"; type Props = EditorProps; +export const SCHEMAS = [ + { name: "Zoo", schema: Zoo }, + // TODO: Residential units + // TODO: Residential units (GLA) +]; + function ListComponent(props: Props) { const formik = useFormik({ initialValues: parseContent(props.node?.data), @@ -21,7 +36,71 @@ function ListComponent(props: Props) { return ( ); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts index 6c0d6e3a88..e420fbb4c4 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts @@ -1,4 +1,5 @@ import { gql } from "@apollo/client"; +import { FlowStatus } from "@opensystemslab/planx-core/types"; import camelcaseKeys from "camelcase-keys"; import { client } from "lib/graphql"; import { @@ -15,6 +16,9 @@ import { TeamStore } from "./team"; export interface SettingsStore { flowSettings?: FlowSettings; setFlowSettings: (flowSettings?: FlowSettings) => void; + flowStatus?: FlowStatus; + setFlowStatus: (flowStatus: FlowStatus) => void; + updateFlowStatus: (newStatus: FlowStatus) => Promise; globalSettings?: GlobalSettings; setGlobalSettings: (globalSettings: GlobalSettings) => void; updateFlowSettings: (newSettings: FlowSettings) => Promise; @@ -33,6 +37,19 @@ export const settingsStore: StateCreator< setFlowSettings: (flowSettings) => set({ flowSettings }), + flowStatus: undefined, + + setFlowStatus: (flowStatus) => set({ flowStatus }), + + updateFlowStatus: async (newStatus) => { + const { id, $client } = get(); + const result = await $client.flow.setStatus({ + flow: { id }, + status: newStatus, + }); + return Boolean(result?.id); + }, + globalSettings: undefined, setGlobalSettings: (globalSettings) => { diff --git a/editor.planx.uk/src/pages/Preview/Node.tsx b/editor.planx.uk/src/pages/Preview/Node.tsx index b2a22b4dec..eb6ace5e29 100644 --- a/editor.planx.uk/src/pages/Preview/Node.tsx +++ b/editor.planx.uk/src/pages/Preview/Node.tsx @@ -30,6 +30,7 @@ import Send from "@planx/components/Send/Public"; import SetValue from "@planx/components/SetValue/Public"; import TaskList from "@planx/components/TaskList/Public"; import TextInput from "@planx/components/TextInput/Public"; +import { hasFeatureFlag } from "lib/featureFlags"; import { objectWithoutNullishValues } from "lib/objectHelpers"; import mapAccum from "ramda/src/mapAccum"; import React from "react"; @@ -169,7 +170,7 @@ const Node: React.FC = (props: Props) => { return ; case TYPES.List: - return ; + return hasFeatureFlag("LIST_COMPONENT") ? : null; case TYPES.NextSteps: return ; diff --git a/editor.planx.uk/src/routes/flowSettings.tsx b/editor.planx.uk/src/routes/flowSettings.tsx index 38c1f60e7d..b06ce1406e 100644 --- a/editor.planx.uk/src/routes/flowSettings.tsx +++ b/editor.planx.uk/src/routes/flowSettings.tsx @@ -1,5 +1,5 @@ +import { FlowStatus } from "@opensystemslab/planx-core/types"; import gql from "graphql-tag"; -import { publicClient } from "lib/graphql"; import { compose, map, @@ -17,6 +17,7 @@ import Submissions from "pages/FlowEditor/components/Settings/Submissions"; import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; +import { client } from "../lib/graphql"; import Settings, { SettingsTab } from "../pages/FlowEditor/components/Settings"; import type { FlowSettings } from "../types"; import { makeTitle } from "./utils"; @@ -25,15 +26,16 @@ interface GetFlowSettings { flows: { id: string; settings: FlowSettings; + status: FlowStatus; }[]; } export const getFlowSettings = async (req: NaviRequest) => { const { data: { - flows: [{ settings }], + flows: [{ settings, status }], }, - } = await publicClient.query({ + } = await client.query({ query: gql` query GetFlow($slug: String!, $team_slug: String!) { flows( @@ -42,6 +44,7 @@ export const getFlowSettings = async (req: NaviRequest) => { ) { id settings + status } } `, @@ -52,6 +55,7 @@ export const getFlowSettings = async (req: NaviRequest) => { }); useStore.getState().setFlowSettings(settings); + useStore.getState().setFlowStatus(status); }; const tabs: SettingsTab[] = [ diff --git a/editor.planx.uk/src/theme.ts b/editor.planx.uk/src/theme.ts index d42b363b88..69dbb6c177 100644 --- a/editor.planx.uk/src/theme.ts +++ b/editor.planx.uk/src/theme.ts @@ -57,13 +57,13 @@ const DEFAULT_PALETTE: Partial = { action: { selected: "#F8F8F8", focus: "#FFDD00", - disabled: "#B4B4B4", + disabled: "#949494", }, error: { main: "#D4351C", }, success: { - main: "#4CAF50", + main: "#49A74C", dark: "#265A26", }, info: { diff --git a/editor.planx.uk/src/ui/editor/SelectInput.tsx b/editor.planx.uk/src/ui/editor/SelectInput.tsx index 61954a5ae1..926e01b58b 100644 --- a/editor.planx.uk/src/ui/editor/SelectInput.tsx +++ b/editor.planx.uk/src/ui/editor/SelectInput.tsx @@ -9,6 +9,7 @@ export interface Props extends SelectProps { name?: string; children?: ReactNode; onChange?: SelectProps["onChange"]; + bordered?: boolean; } const PREFIX = "SelectInput"; @@ -77,6 +78,7 @@ export default function SelectInput({ value, name, onChange, + bordered, ...props }: Props): FCReturn { return ( @@ -89,7 +91,7 @@ export default function SelectInput({ }} onChange={onChange} IconComponent={ArrowIcon} - input={} + input={} inputProps={{ name, classes: { diff --git a/editor.planx.uk/src/ui/icons/Editor.tsx b/editor.planx.uk/src/ui/icons/Editor.tsx index b318f36a71..d01640db67 100644 --- a/editor.planx.uk/src/ui/icons/Editor.tsx +++ b/editor.planx.uk/src/ui/icons/Editor.tsx @@ -10,7 +10,7 @@ export default function EditorIcon(props: SvgIconProps) { > ); diff --git a/editor.planx.uk/src/ui/public/InputLabel.tsx b/editor.planx.uk/src/ui/public/InputLabel.tsx index 0ee71532dc..816006a974 100644 --- a/editor.planx.uk/src/ui/public/InputLabel.tsx +++ b/editor.planx.uk/src/ui/public/InputLabel.tsx @@ -13,9 +13,10 @@ export default function InputLabel(props: { children: ReactNode; hidden?: boolean; htmlFor?: string; + id?: string; }) { return ( - + )} - - -