Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: added Int Portal restyling to Ext Portal #3998

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,22 +9,31 @@
test("adding an external portal", async () => {
const handleSubmit = vi.fn();

setup(
const { user } = setup(
<ExternalPortalForm
flows={[
{ id: "a", text: "flow a" },
{ id: "b", text: "flow b" },
{ id: "a", name: "flow a", slug: "flow-a", team: "team" },
{ id: "b", name: "flow b", slug: "flow-b", team: "team" },
]}
handleSubmit={handleSubmit}
/>,
);
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("");

Check failure on line 26 in editor.planx.uk/src/@planx/components/ExternalPortal/Editor.test.tsx

View workflow job for this annotation

GitHub Actions / Run React Tests

src/@planx/components/ExternalPortal/Editor.test.tsx > adding an external portal

Error: expect(element).toHaveValue() Expected the element to have value: Received: flow a ❯ src/@planx/components/ExternalPortal/Editor.test.tsx:26:29

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({
Expand All @@ -41,24 +50,31 @@
test("changing an external portal", async () => {
const handleSubmit = vi.fn();

setup(
const { user } = setup(
<ExternalPortalForm
id="test"
flowId="b"
flows={[
{ id: "a", text: "flow a" },
{ id: "b", text: "flow b" },
{ id: "a", name: "flow a", slug: "flow-a", team: "team" },
{ id: "b", name: "flow b", slug: "flow-b", team: "team" },
]}
handleSubmit={handleSubmit}
/>,
);

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({
Expand Down
190 changes: 172 additions & 18 deletions editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<ArrowIcon
sx={(theme) => ({ 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 (
<MenuItem
{...props}
id={option.id}
data-testid={`flow-${option.id}`}
sx={(theme) => ({ paddingY: `${theme.spacing(1.25)}` })}
>
{option.name}
</MenuItem>
);
};

const renderInput: FlowAutocompleteInputProps["renderInput"] = (params) => (
<StyledTextField
{...params}
id={params.id}
InputProps={{
...params.InputProps,
notched: false,
}}
key={params.id}
/>
);

const renderGroup: FlowAutocompleteListProps["renderGroup"] = (params) => {
return (
<React.Fragment key={params.group}>
<AutocompleteSubHeader
disableSticky
id={`group-header-${params.group}`}
aria-label={params.group}
>
<Typography variant="subtitle2" component="h4" py={1.5}>
{params.group}
</Typography>
</AutocompleteSubHeader>
{params.children}
</React.Fragment>
);
};

const ExternalPortalForm: React.FC<{
id?: string;
flowId?: string;
notes?: string;
handleSubmit?: (val: any) => void;
flows?: Array<Flow>;
tags?: NodeTag[];
}> = ({ id, handleSubmit, flowId = "", flows = [], tags = [], notes = "" }) => {
}> = ({ handleSubmit, flowId = "", flows = [], tags = [], notes = "" }) => {
const [teamArray, setTeamArray] = useState<string[]>([]);
const [firstFlow, setFirstFlow] = useState<Flow>(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,
Expand Down Expand Up @@ -51,20 +155,70 @@ const ExternalPortalForm: React.FC<{
flow that it references.
</span>
</ModalSectionContent>
<ModalSectionContent title="Pick a flow">
<select
<ModalSectionContent key={"team-section"} title="Select a team">
<SelectMultiple
id="team-select"
onChange={(_options, event) => {
setTeamArray([...event]);
}}
value={teamArray}
options={uniqueTeamArray}
renderOption={(props, option, { selected }) => (
<ListItem {...props} key={`${option}-listitem`}>
<CustomCheckbox
key={`${option}-checkbox`}
aria-hidden="true"
className={selected ? "selected" : ""}
/>
{option}
</ListItem>
)}
placeholder=""
/>
</ModalSectionContent>
<ModalSectionContent key={"flow-section"} title="Pick a flow">
<Autocomplete
data-testid="flowId"
name="flowId"
value={formik.values.flowId}
onChange={formik.handleChange}
>
{!id && <option value="" />}
{flows.map((flow) => (
<option key={flow.id} value={flow.id}>
{flow.text}
</option>
))}
</select>
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",
},
}}
/>
</ModalSectionContent>
</ModalSection>
<ModalFooter formik={formik} showMoreInformation={false} />
Expand Down
18 changes: 15 additions & 3 deletions editor.planx.uk/src/routes/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ const getExternalPortals = async () => {
flows(order_by: { slug: asc }) {
id
slug
name
team {
slug
name
}
}
}
Expand All @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions editor.planx.uk/src/ui/shared/SelectMultiple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`]: {
Expand Down Expand Up @@ -114,7 +114,7 @@ export const CustomCheckbox = styled("span")(({ theme }) => ({
export function SelectMultiple<T>(props: Props<T>) {
// 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 (
<FormControl sx={{ display: "flex", flexDirection: "column" }}>
Expand Down
Loading