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 (
+
+ );
+};
+
+const renderInput: FlowAutocompleteInputProps["renderInput"] = (params) => (
+
+);
+
+const renderGroup: FlowAutocompleteListProps["renderGroup"] = (params) => {
+ return (
+
+
+ {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 && }
- {flows.map((flow) => (
-
- ))}
-
+ 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 (