diff --git a/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.test.tsx b/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.test.tsx index 792e6b0907..8b52b345de 100644 --- a/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.test.tsx +++ b/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.test.tsx @@ -1,5 +1,5 @@ import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; -import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import React from "react"; import { setup } from "testUtils"; import { vi } from "vitest"; @@ -9,22 +9,31 @@ import ExternalPortalForm from "./Editor"; test("adding an external portal", async () => { const handleSubmit = vi.fn(); - setup( + const { user } = setup( , ); + const autocompleteComp = screen.getByTestId("flowId"); + const autocompleteInput = within(autocompleteComp).getByRole("combobox"); - expect(screen.getByTestId("flowId")).toHaveValue(""); + screen.debug(autocompleteInput); - await fireEvent.change(screen.getByTestId("flowId"), { - target: { value: "b" }, - }); - await fireEvent.submit(screen.getByTestId("form")); + expect(autocompleteInput).toHaveValue(""); + + await user.click(autocompleteInput); + + await user.click(screen.getByTestId("flow-b")); + + expect(autocompleteInput).toHaveValue("flow b"); + + const extPortalForm = screen.getByTestId("form"); + + fireEvent.submit(extPortalForm); await waitFor(() => expect(handleSubmit).toHaveBeenCalledWith({ @@ -41,24 +50,31 @@ test("adding an external portal", async () => { test("changing an external portal", async () => { const handleSubmit = vi.fn(); - setup( + const { user } = setup( , ); - expect(screen.getByTestId("flowId")).toHaveValue("b"); + const autocompleteComp = screen.getByTestId("flowId"); + const autocompleteInput = within(autocompleteComp).getByRole("combobox"); + + expect(autocompleteInput).toHaveValue("flow b"); + + await user.click(autocompleteInput); + + await user.click(screen.getByTestId("flow-a")); + + expect(autocompleteInput).toHaveValue("flow a"); + + const extPortalForm = screen.getByTestId("form"); - await fireEvent.change(screen.getByTestId("flowId"), { - target: { value: "a" }, - }); - await fireEvent.submit(screen.getByTestId("form")); + fireEvent.submit(extPortalForm); await waitFor(() => expect(handleSubmit).toHaveBeenCalledWith({ diff --git a/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx b/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx index 47d8fafc6a..57ecc2f4ab 100644 --- a/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx @@ -1,28 +1,132 @@ +import Autocomplete, { + autocompleteClasses, + AutocompleteProps, +} from "@mui/material/Autocomplete"; +import ListItem from "@mui/material/ListItem"; +import ArrowIcon from "@mui/icons-material/KeyboardArrowDown"; +import ListSubheader from "@mui/material/ListSubheader"; +import MenuItem from "@mui/material/MenuItem"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; import { ComponentType as TYPES, NodeTag, } from "@opensystemslab/planx-core/types"; import { useFormik } from "formik"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { ModalFooter } from "ui/editor/ModalFooter"; import ModalSection from "ui/editor/ModalSection"; import ModalSectionContent from "ui/editor/ModalSectionContent"; +import { + CustomCheckbox, + SelectMultiple, + StyledTextField, +} from "ui/shared/SelectMultiple"; import { ICONS } from "../shared/icons"; interface Flow { - id: string; - text: string; + id: string | "select a flow"; + slug: string; + name: string; + team: string; } +type FlowAutocompleteListProps = AutocompleteProps< + Flow, + false, + true, + false, + "li" +>; +type FlowAutocompleteInputProps = AutocompleteProps< + Flow, + false, + true, + false, + "input" +>; + +const PopupIcon = ( + ({ color: theme.palette.primary.main })} + fontSize="large" + /> +); + +const AutocompleteSubHeader = styled(ListSubheader)(({ theme }) => ({ + border: "none", + borderTop: `1px solid ${theme.palette.border.main}`, + backgroundColor: theme.palette.background.default, +})); + +const renderOption: FlowAutocompleteListProps["renderOption"] = ( + props, + option +) => { + return ( + ({ paddingY: `${theme.spacing(1.25)}` })} + > + {option.name} + + ); +}; + +const renderInput: FlowAutocompleteInputProps["renderInput"] = (params) => ( + +); + +const renderGroup: FlowAutocompleteListProps["renderGroup"] = (params) => { + return ( + + + + {params.group} + + + {params.children} + + ); +}; + const ExternalPortalForm: React.FC<{ - id?: string; flowId?: string; notes?: string; handleSubmit?: (val: any) => void; flows?: Array; tags?: NodeTag[]; -}> = ({ id, handleSubmit, flowId = "", flows = [], tags = [], notes = "" }) => { +}> = ({ handleSubmit, flowId = "", flows = [], tags = [], notes = "" }) => { + const [teamArray, setTeamArray] = useState([]); + const [firstFlow, setFirstFlow] = useState(flows[0]); + + const uniqueTeamArray = [...new Set(flows.map((item) => item.team))]; + + useEffect(() => { + const filterFlows = () => { + const filteredFlows = flows.filter((flow: Flow) => + teamArray.includes(flow.team) + ); + filteredFlows[0] && setFirstFlow(filteredFlows[0]); + }; + filterFlows(); + }, [teamArray, flows]); + const formik = useFormik({ initialValues: { flowId, @@ -51,20 +155,70 @@ const ExternalPortalForm: React.FC<{ flow that it references. - - + id="flowId" + role="status" + aria-atomic={true} + aria-live="polite" + fullWidth + popupIcon={PopupIcon} + ListboxProps={{ + sx: (theme) => ({ + paddingY: 0, + backgroundColor: theme.palette.background.default, + }), + }} + value={ + flows.find((flow: Flow) => flow.id === formik.values.flowId) || + firstFlow + } + onChange={(_event, newValue: Flow) => { + formik.setFieldValue("flowId", newValue.id); + }} + options={flows.filter((flow) => { + if (teamArray.length > 0) return teamArray.includes(flow.team); + return true; + })} + groupBy={(option) => option.team} + getOptionLabel={(option) => option.name} + renderOption={renderOption} + renderInput={renderInput} + renderGroup={renderGroup} + slotProps={{ + popper: { + placement: "bottom-start", + modifiers: [{ name: "flip", enabled: false }], + }, + }} + sx={{ + [`& .${autocompleteClasses.endAdornment}`]: { + top: "unset", + }, + }} + /> diff --git a/editor.planx.uk/src/routes/flow.tsx b/editor.planx.uk/src/routes/flow.tsx index bc089262cb..e1c67c9c3b 100644 --- a/editor.planx.uk/src/routes/flow.tsx +++ b/editor.planx.uk/src/routes/flow.tsx @@ -34,8 +34,10 @@ const getExternalPortals = async () => { flows(order_by: { slug: asc }) { id slug + name team { slug + name } } } @@ -48,11 +50,21 @@ const getExternalPortals = async () => { flow.team && !window.location.pathname.includes(`${flow.team.slug}/${flow.slug}`), ) - .map(({ id, team, slug }: Flow) => ({ + .map(({ id, team, slug, name }: Flow) => ({ id, - text: [team.slug, slug].join("/"), + name, + slug, + team: team.name, })) - .sort(sortFlows); + .sort((a: Flow, b: Flow) => { + if (a.team > b.team) { + return 1; + } else if (b.team > a.team) { + return -1; + } else { + return 0; + } + }); }; const newNode = route(async (req) => { diff --git a/editor.planx.uk/src/ui/shared/SelectMultiple.tsx b/editor.planx.uk/src/ui/shared/SelectMultiple.tsx index 8de9e6c9c4..a53c8134b1 100644 --- a/editor.planx.uk/src/ui/shared/SelectMultiple.tsx +++ b/editor.planx.uk/src/ui/shared/SelectMultiple.tsx @@ -55,7 +55,7 @@ const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({ }, })) as typeof Autocomplete; -const StyledTextField = styled(TextField)(({ theme }) => ({ +export const StyledTextField = styled(TextField)(({ theme }) => ({ "&:focus-within": { ...borderedFocusStyle, [`& .${outlinedInputClasses.notchedOutline}`]: { @@ -114,7 +114,7 @@ export const CustomCheckbox = styled("span")(({ theme }) => ({ export function SelectMultiple(props: Props) { // MUI doesn't pass the Autocomplete value along to the TextField automatically const isSelectEmpty = !props.value?.length; - const placeholder = isSelectEmpty ? props.placeholder : undefined + const placeholder = isSelectEmpty ? props.placeholder : undefined; return (