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

feat: add tags to portals #3975

Merged
merged 5 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test("adding an external portal", async () => {
type: TYPES.ExternalPortal,
data: {
flowId: "b",
tags: [],
},
}),
);
Expand Down Expand Up @@ -63,6 +64,7 @@ test("changing an external portal", async () => {
type: TYPES.ExternalPortal,
data: {
flowId: "a",
tags: [],
},
}),
);
Expand Down
14 changes: 12 additions & 2 deletions editor.planx.uk/src/@planx/components/ExternalPortal/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import {
ComponentType as TYPES,
NodeTag,
} from "@opensystemslab/planx-core/types";
import { useFormik } from "formik";
import React from "react";
import { ComponentTagSelect } from "ui/editor/ComponentTagSelect";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";

Expand All @@ -16,10 +20,12 @@ const ExternalPortalForm: React.FC<{
flowId?: string;
handleSubmit?: (val: any) => void;
flows?: Array<Flow>;
}> = ({ id, handleSubmit, flowId = "", flows = [] }) => {
tags?: NodeTag[];
}> = ({ id, handleSubmit, flowId = "", flows = [], tags = [] }) => {
const formik = useFormik({
initialValues: {
flowId,
tags,
},
onSubmit: (values) => {
if (handleSubmit) {
Expand Down Expand Up @@ -58,6 +64,10 @@ const ExternalPortalForm: React.FC<{
))}
</select>
</ModalSectionContent>
<ComponentTagSelect
value={formik.values.tags}
onChange={(value) => formik.setFieldValue("tags", value)}
/>
</ModalSection>
</form>
);
Expand Down
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 @@ -11,33 +11,34 @@ describe("adding an internal portal", () => {
test("creating a new internal portal", async () => {
const handleSubmit = vi.fn();

const { user } = setup(
const { user, getByTestId } = setup(
<InternalPortalForm
flows={[{ id: "ignore", text: "ignore" }]}
handleSubmit={handleSubmit}
/>,
);

const flowSelect = screen.getByTestId("flowId");
const existingFlowInput = getByTestId("flowId");
const flowSelect = within(existingFlowInput).getByRole("combobox");

expect(flowSelect).toHaveValue("");
expect(flowSelect).toBeEnabled();

await user.type(
screen.getByPlaceholderText("Portal name"),
screen.getByPlaceholderText("Enter a portal name"),
"new internal portal",
);

expect(flowSelect).toBeDisabled();
expect(flowSelect).toHaveAttribute("aria-disabled", "true");

await fireEvent.submit(screen.getByTestId("form"));
fireEvent.submit(screen.getByTestId("form"));

await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
type: TYPES.InternalPortal,
data: {
flowId: "", // will be removed when saving the data
text: "new internal portal",
tags: [],
},
});
});
Expand All @@ -53,15 +54,20 @@ describe("adding an internal portal", () => {
/>,
);

const dropdown = screen.queryByTestId("flowId");
const dropdown = await screen.findByRole("combobox", {
name: "Use an existing portal",
});
expect(dropdown).toBeInTheDocument();

await user.click(dropdown);

expect(dropdown).toHaveValue("");
const option = await screen.findByRole("option", { name: "portal" });
expect(option).toBeInTheDocument();

if (dropdown) {
await user.selectOptions(dropdown, "portal");
}
await user.click(option);

fireEvent.submit(screen.getByTestId("form"));

await fireEvent.submit(screen.getByTestId("form"));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith("portal");
});
Expand All @@ -72,17 +78,24 @@ describe("adding an internal portal", () => {

const { user } = setup(
<InternalPortalForm
flows={[{ id: "portal", text: "portal" }]}
flows={[{ id: "portal", text: "portal text" }]}
handleSubmit={handleSubmit}
/>,
);

const dropdown = screen.queryByTestId("flowId");
if (dropdown) {
await user.selectOptions(dropdown, "portal");
}
const dropdown = await screen.findByRole("combobox", {
name: "Use an existing portal",
});
expect(dropdown).toBeInTheDocument();

await user.click(dropdown);

const option = await screen.findByRole("option", { name: "portal text" });
expect(option).toBeInTheDocument();

await user.click(option);
fireEvent.submit(screen.getByTestId("form"));

await fireEvent.submit(screen.getByTestId("form"));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith("portal");
});
Expand All @@ -103,20 +116,21 @@ test("updating an internal portal", async () => {

expect(screen.queryByTestId("flowId")).not.toBeInTheDocument();

const textInput = screen.getByPlaceholderText("Portal name");
const textInput = screen.getByPlaceholderText("Enter a portal name");

expect(textInput).toHaveValue("val");

await user.clear(textInput);
await user.type(textInput, "new val");
await fireEvent.submit(screen.getByTestId("form"));
fireEvent.submit(screen.getByTestId("form"));

await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
type: TYPES.InternalPortal,
data: {
flowId: "", // will be removed when saving the data
text: "new val",
tags: [],
},
});
});
Expand All @@ -125,18 +139,22 @@ test("updating an internal portal", async () => {
describe("validations", () => {
describe("if no flowId is chosen", () => {
const scenarios = [
{ action: "adding without flows", error: "Required." },
{ action: "updating without flows", id: "test", error: "Required." },
{ action: "adding without flows", error: "Enter a portal name" },
{
action: "updating without flows",
id: "test",
error: "Enter a portal name",
},
{
action: "adding with flows",
flows: [{ id: "portal", text: "portal" }],
error: "Required if no flow is selected",
error: "Enter a portal name or select an existing portal",
},
{
action: "updating with flows",
flows: [{ id: "portal", text: "portal" }],
id: "test",
error: "Required if no flow is selected",
error: "Enter a portal name or select an existing portal",
},
];
for (const scenario of scenarios) {
Expand Down
112 changes: 66 additions & 46 deletions editor.planx.uk/src/@planx/components/InternalPortal/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import MenuItem from "@mui/material/MenuItem";
import {
ComponentType as TYPES,
NodeTag,
} from "@opensystemslab/planx-core/types";
import { useFormik } from "formik";
import React from "react";
import InputField from "ui/editor/InputField/InputField";
import { ComponentTagSelect } from "ui/editor/ComponentTagSelect";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import SelectInput from "ui/editor/SelectInput/SelectInput";
import InputLabel from "ui/public/InputLabel";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";

import { FormError } from "../shared/types";
import { ICONS } from "../shared/icons";

interface Flow {
id: string;
Expand All @@ -16,20 +26,22 @@ const InternalPortalForm: React.FC<{
flowId?: string;
handleSubmit?: (val: any) => void;
flows?: Array<Flow>;
}> = ({ id, handleSubmit, text = "", flowId = "", flows = [] }) => {
tags?: NodeTag[];
}> = ({ handleSubmit, text = "", flowId = "", flows = [], tags = [] }) => {
const formik = useFormik({
initialValues: {
text,
flowId,
tags,
},
validate: (values) => {
const errors: Record<string, string> = {};

if (!values.flowId && !values.text) {
errors.text =
flows.length > 0 ? "Required if no flow is selected" : "Required.";
flows.length > 0
? "Enter a portal name or select an existing portal"
: "Enter a portal name";
}

return errors;
},
onSubmit: (values) => {
Expand All @@ -46,46 +58,54 @@ const InternalPortalForm: React.FC<{

return (
<form id="modal" onSubmit={formik.handleSubmit} data-testid="form">
<div>
<label htmlFor="portalFlowId">Create new internal portal: </label>
<InputField
name="text"
onChange={formik.handleChange}
placeholder="Portal name"
rows={2}
value={formik.values.text}
disabled={!!formik.values.flowId}
id="portalFlowId"
// required={!formik.values.flowId} (was ignored by @testing-library?)
<ModalSection>
<ModalSectionContent
title="Internal Portal"
Icon={ICONS[TYPES.InternalPortal]}
>
<ErrorWrapper error={formik.errors.text}>
<Input
name="text"
onChange={formik.handleChange}
placeholder="Enter a portal name"
rows={2}
value={formik.values.text}
disabled={!!formik.values.flowId}
id="portalFlowId"
/>
</ErrorWrapper>
</ModalSectionContent>
{flows?.length > 0 && (
<ModalSectionContent subtitle="Use an existing portal">
<InputLabel
label="Use an existing portal"
id="flowId-label"
hidden
htmlFor="flowId"
/>
<SelectInput
aria-describedby="flowId"
labelId="flowId-label"
id="flowId"
data-testid="flowId"
name="flowId"
value={formik.values.flowId}
onChange={formik.handleChange}
disabled={!!formik.values.text}
>
{flows.map((flow) => (
<MenuItem key={flow.id} value={flow.id}>
{flow.text}
</MenuItem>
))}
</SelectInput>
</ModalSectionContent>
)}
<ComponentTagSelect
value={formik.values.tags}
onChange={(value) => formik.setFieldValue("tags", value)}
/>
{formik.errors.text && <FormError message={formik.errors.text} />}
</div>
{flows?.length > 0 && (
<>
<span>
<br /> OR
<br />
<br />
<label htmlFor="flowId">Point to an existing portal:</label>
<br />
</span>
<select
id="flowId"
data-testid="flowId"
name="flowId"
value={formik.values.flowId}
onChange={formik.handleChange}
disabled={!!formik.values.text}
>
{!id && <option value="" />}
{flows.map((flow) => (
<option key={flow.id} value={flow.id}>
{flow.text}
</option>
))}
</select>
</>
)}
</ModalSection>
</form>
);
};
Expand Down
Loading
Loading