diff --git a/README.md b/README.md index 652f2bf647..65b5832e4e 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,14 @@ We'd love to hear what you're building on Planx, don't hesitate to get in touch The root of the project has several scripts set up to help you manage your docker containers: -- `pnpm run up` alias for `pnpm recreate && pnpm add-data` +- `pnpm run up` alias for `pnpm recreate && pnpm sync-data` - `pnpm run down` alias for `pnpm destroy` - `pnpm run restart` alias for `pnpm stop && pnpm start` - `pnpm start` will (re)create docker containers without rebuilding them - `pnpm stop` will stop your docker containers without destroying them - `pnpm recreate` will build and (re)start your docker containers from scratch. - `pnpm destroy` will remove volumes (i.e. database data) and can be a useful hard reset when necessary. -- `pnpm add-data` will sync production records with modified data in your database +- `pnpm sync-data` will sync production records with modified data in your database - `pnpm clean-data` will sync production records and reset any modified data - `pnpm tests` will recreate your docker containers and include test services - `pnpm analytics` will recreate your docker containers and include [Metabase](https://www.metabase.com/) diff --git a/api.planx.uk/modules/auth/controller.ts b/api.planx.uk/modules/auth/controller.ts index b1b5394193..a03fdc4b17 100644 --- a/api.planx.uk/modules/auth/controller.ts +++ b/api.planx.uk/modules/auth/controller.ts @@ -7,14 +7,6 @@ export const failedLogin: RequestHandler = (_req, _res, next) => message: "User failed to authenticate", }); -export const logout: RequestHandler = (req, res) => { - // TODO: implement dual purpose as Microsoft frontend logout channel - req.logout(() => { - // do nothing - }); - res.redirect(process.env.EDITOR_URL_EXT!); -}; - export const handleSuccess = (req: Request, res: Response) => { if (!req.user) { return res.json({ diff --git a/api.planx.uk/modules/auth/docs.yaml b/api.planx.uk/modules/auth/docs.yaml index 673e45e531..6377d20268 100644 --- a/api.planx.uk/modules/auth/docs.yaml +++ b/api.planx.uk/modules/auth/docs.yaml @@ -6,13 +6,6 @@ tags: - name: auth description: Authentication related requests paths: - /logout: - get: - summary: Logout from the PlanX service - tags: ["auth"] - responses: - "302": - description: Redirect to PlanX Editor /auth/login/failed: get: summary: Failed login @@ -44,3 +37,19 @@ paths: responses: "200": description: OK + /auth/microsoft: + get: + summary: Authenticate via Microsoft SSO + description: The first step in Microsoft authentication will involve redirecting the user to login.microsoftonline.com + tags: ["auth"] + responses: + "200": + description: OK + /auth/microsoft/callback: + get: + summary: Generate a JWT for an authenticated user + description: After authentication, Microsoft will redirect the user back to this route which generates a JWT for the user + tags: ["auth"] + responses: + "200": + description: OK diff --git a/api.planx.uk/modules/auth/routes.ts b/api.planx.uk/modules/auth/routes.ts index d349f55d94..1cf8d7771c 100644 --- a/api.planx.uk/modules/auth/routes.ts +++ b/api.planx.uk/modules/auth/routes.ts @@ -6,8 +6,6 @@ import * as Controller from "./controller.js"; export default (passport: Authenticator): Router => { const router = Router(); - router.get("/logout", Controller.logout); - // router.get("/auth/frontchannel-logout", Controller.frontChannelLogout) router.get("/auth/login/failed", Controller.failedLogin); router.get("/auth/google", Middleware.getGoogleAuthHandler(passport)); router.get( diff --git a/e2e/tests/ui-driven/src/create-flow-with-geospatial.spec.ts b/e2e/tests/ui-driven/src/create-flow-with-geospatial.spec.ts index 691f83a257..991dcd0bc5 100644 --- a/e2e/tests/ui-driven/src/create-flow-with-geospatial.spec.ts +++ b/e2e/tests/ui-driven/src/create-flow-with-geospatial.spec.ts @@ -62,7 +62,7 @@ test.describe("Flow creation, publish and preview", () => { await expect(editor.nodeList).toContainText([ "Find property", "an internal portalEdit Portal", - "(Flags Filter)ImmuneMissing informationPermission neededPrior approvalNoticePermitted developmentNot development(No Result)", + "Filter - Planning permissionImmuneMissing informationPermission neededPrior approvalNoticePermitted developmentNot developmentNo flag result", "Upload and label", "Confirm your location plan", "Planning constraints", diff --git a/editor.planx.uk/src/@planx/components/Filter/Editor.test.tsx b/editor.planx.uk/src/@planx/components/Filter/Editor.test.tsx new file mode 100644 index 0000000000..950d00b272 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/Filter/Editor.test.tsx @@ -0,0 +1,141 @@ +import { + ComponentType as TYPES, + DEFAULT_FLAG_CATEGORY, + flatFlags, +} from "@opensystemslab/planx-core/types"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import React from "react"; +import { setup } from "testUtils"; +import { vi } from "vitest"; + +import Filter from "./Editor"; + +test("Adding a filter without explicit props uses the default flagset", async () => { + const handleSubmit = vi.fn(); + + setup(); + + expect(screen.getByTestId("flagset-category-select")).toHaveValue( + DEFAULT_FLAG_CATEGORY, + ); + + fireEvent.submit(screen.getByTestId("filter-component-form")); + + await waitFor(() => + expect(handleSubmit).toHaveBeenCalledWith( + { + type: TYPES.Filter, + data: { + fn: "flag", + category: DEFAULT_FLAG_CATEGORY, + }, + }, + mockDefaultFlagOptions, + ), + ); +}); + +test("Adding a filter and selecting a flagset category", async () => { + const handleSubmit = vi.fn(); + + setup(); + + expect(screen.getByTestId("flagset-category-select")).toHaveValue( + DEFAULT_FLAG_CATEGORY, + ); + + fireEvent.change(screen.getByTestId("flagset-category-select"), { + target: { value: "Community infrastructure levy" }, + }); + + fireEvent.submit(screen.getByTestId("filter-component-form")); + + await waitFor(() => + expect(handleSubmit).toHaveBeenCalledWith( + { + type: TYPES.Filter, + data: { + fn: "flag", + category: "Community infrastructure levy", + }, + }, + mockCILFlagOptions, + ), + ); +}); + +test("Updating an existing filter to another category", async () => { + const handleSubmit = vi.fn(); + + setup( + , + ); + + expect(screen.getByTestId("flagset-category-select")).toHaveValue( + "Listed building consent", + ); + + fireEvent.change(screen.getByTestId("flagset-category-select"), { + target: { value: "Community infrastructure levy" }, + }); + + fireEvent.submit(screen.getByTestId("filter-component-form")); + + await waitFor(() => + expect(handleSubmit).toHaveBeenCalledWith( + { + type: TYPES.Filter, + data: { + fn: "flag", + category: "Community infrastructure levy", + }, + }, + mockCILFlagOptions, + ), + ); +}); + +const mockExistingFilterNode = { + data: { + fn: "flag", + category: "Listed building consent", + }, + type: 500, + edges: ["flag1", "flag2", "flag3", "flag4", "blankFlag"], +}; + +const mockDefaultFlagOptions = [ + ...flatFlags.filter((flag) => flag.category === DEFAULT_FLAG_CATEGORY), + { + category: DEFAULT_FLAG_CATEGORY, + text: "No flag result", + value: "", + }, +].map((flag) => ({ + type: TYPES.Answer, + data: { + text: flag.text, + val: flag.value, + }, +})); + +const mockCILFlagOptions = [ + ...flatFlags.filter( + (flag) => flag.category === "Community infrastructure levy", + ), + { + category: "Community infrastructure levy", + text: "No flag result", + value: "", + }, +].map((flag) => ({ + type: TYPES.Answer, + data: { + text: flag.text, + val: flag.value, + }, +})); diff --git a/editor.planx.uk/src/@planx/components/Filter/Editor.tsx b/editor.planx.uk/src/@planx/components/Filter/Editor.tsx index 5d2f3f2b03..b7b84f8ec3 100644 --- a/editor.planx.uk/src/@planx/components/Filter/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Filter/Editor.tsx @@ -1,52 +1,84 @@ +import Typography from "@mui/material/Typography"; import { + ComponentType as TYPES, DEFAULT_FLAG_CATEGORY, flatFlags, } from "@opensystemslab/planx-core/types"; -import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import { useFormik } from "formik"; import React from "react"; +import ModalSection from "ui/editor/ModalSection"; +import ModalSectionContent from "ui/editor/ModalSectionContent"; + +import { ICONS } from "../ui"; export interface Props { id?: string; - handleSubmit?: (d: any, c?: any) => void; + handleSubmit?: (data: any, children?: any) => void; node?: any; } const Filter: React.FC = (props) => { const formik = useFormik({ - initialValues: {}, + initialValues: { + fn: "flag", + category: props?.node?.data?.category || DEFAULT_FLAG_CATEGORY, + }, onSubmit: (newValues) => { - if (props.handleSubmit) { - const children = props.id - ? undefined - : [ - ...flatFlags, - { - category: DEFAULT_FLAG_CATEGORY, - text: "(No Result)", - value: "", - }, - ] - .filter((f) => f.category === DEFAULT_FLAG_CATEGORY) - .map((f) => ({ - type: TYPES.Answer, - data: { - text: f.text, - val: f.value, - }, - })); + if (props?.handleSubmit) { + const children = [ + ...flatFlags, + { + category: formik.values.category, + text: "No flag result", + value: "", + }, + ] + .filter((f) => f.category === formik.values.category) + .map((f) => ({ + type: TYPES.Answer, + data: { + text: f.text, + val: f.value, + }, + })); - props.handleSubmit( - { type: TYPES.Filter, data: { newValues, fn: "flag" } }, - children, - ); + props.handleSubmit({ type: TYPES.Filter, data: newValues }, children); } }, - validate: () => {}, }); + + const categories = new Set(flatFlags.map((flag) => flag.category)); + return ( - ); }; diff --git a/editor.planx.uk/src/@planx/components/Filter/model.ts b/editor.planx.uk/src/@planx/components/Filter/model.ts deleted file mode 100644 index 0ca9cf9275..0000000000 --- a/editor.planx.uk/src/@planx/components/Filter/model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Filter { - id?: string; - handleSubmit?: (d: any, children: any) => void; - node?: any; -} diff --git a/editor.planx.uk/src/@planx/components/Result/Editor.tsx b/editor.planx.uk/src/@planx/components/Result/Editor.tsx index 2caa01f7d0..b6a88e4471 100644 --- a/editor.planx.uk/src/@planx/components/Result/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Result/Editor.tsx @@ -1,13 +1,13 @@ import Box from "@mui/material/Box"; -import Collapse from "@mui/material/Collapse"; import Typography from "@mui/material/Typography"; import { Flag, flatFlags } from "@opensystemslab/planx-core/types"; import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import { useFormik } from "formik"; import groupBy from "lodash/groupBy"; -import React, { useState } from "react"; +import React from "react"; import ModalSection from "ui/editor/ModalSection"; import ModalSectionContent from "ui/editor/ModalSectionContent"; +import InputLabel from "ui/public/InputLabel"; import Input from "ui/shared/Input"; import InputRow from "ui/shared/InputRow"; @@ -28,37 +28,25 @@ const FlagEditor: React.FC<{ }> = (props) => { const { flag, existingOverrides } = props; - const [expanded, setExpanded] = useState(false); - - const showEditedIndicator = Boolean(existingOverrides); - return ( - - setExpanded((x) => !x)}> - - {flag.text} - {showEditedIndicator && "*"} - + + + {flag.text} - - + + + props.onChange({ ...existingOverrides, heading: ev.target.value }) } /> - + + props.onChange({ ...existingOverrides, @@ -66,8 +54,8 @@ const FlagEditor: React.FC<{ }) } /> - - + + ); }; @@ -85,7 +73,7 @@ const ResultComponent: React.FC = (props) => { props.handleSubmit({ type: TYPES.Result, data: newValues }); } }, - validate: () => { }, + validate: () => {}, }); const allFlagsForSet = flags[formik.values.flagSet]; @@ -149,4 +137,4 @@ const ResultComponent: React.FC = (props) => { ); }; -export default ResultComponent; \ No newline at end of file +export default ResultComponent; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Node.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Node.tsx index 2537272cb3..4a043a42a7 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Node.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Node.tsx @@ -1,4 +1,7 @@ -import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; +import { + ComponentType as TYPES, + DEFAULT_FLAG_CATEGORY, +} from "@opensystemslab/planx-core/types"; import React from "react"; import { ErrorBoundary } from "react-error-boundary"; import { exhaustiveCheck } from "utils"; @@ -63,7 +66,12 @@ const Node: React.FC = (props) => { /> ); case TYPES.Filter: - return ; + return ( + + ); case TYPES.FindProperty: return ; case TYPES.List: diff --git a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss index e610e515d9..5998350e7b 100644 --- a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss +++ b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss @@ -110,7 +110,8 @@ $fontMonospace: "Source Code Pro", monospace; } } - &.wasVisited a { + &.wasVisited > div, + &.wasVisited > a { span { opacity: 1; } diff --git a/package.json b/package.json index 6fce77c2de..4309ce6171 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "scripts": { - "up": "pnpm recreate && pnpm add-data", + "up": "pnpm recreate && pnpm sync-data", "down": "pnpm destroy", "restart": "pnpm stop && pnpm start", "start": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml --profile mock-services up -d --quiet-pull", "stop": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml --profile mock-services down", "recreate": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml --profile mock-services up -d --quiet-pull --build --renew-anon-volumes --force-recreate", "destroy": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml --profile mock-services down --remove-orphans -v", - "add-data": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml -f ./docker-compose.seed.yml run seed-database", + "sync-data": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml -f ./docker-compose.seed.yml run seed-database", "test-sync": "docker compose -f ./docker-compose.yml -f ./docker-compose.local.yml -f ./docker-compose.seed.yml run seed-database reset_flows", "clean-data": "docker compose -f ./docker-compose.yml -f ./docker-compose.seed.yml run seed-database reset_all", "tests": "./scripts/start-containers-for-tests.sh",