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: display external portals on readme page + test coverage #4177

Merged
merged 46 commits into from
Jan 29, 2025
Merged
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
520b28a
Temp replacement of feedback log with new readme page
jamdelion Jan 13, 2025
e580475
Merge branch 'main' into jh/experimental-readme-page
jamdelion Jan 13, 2025
a74aa58
Give readme page its own folder and route
jamdelion Jan 13, 2025
a1383d7
Make flow and team names accessible in page
jamdelion Jan 13, 2025
50fbea7
Change to rich text inputs, add existing getFlowInfo call
jamdelion Jan 14, 2025
0f79dc4
Replace existing description input from service settings
jamdelion Jan 15, 2025
188ecaa
Be able to edit flow summary and flow description including new column
jamdelion Jan 16, 2025
135cc46
Get summary working correctly with zustand reset
jamdelion Jan 16, 2025
59f6dc1
Fix type issue
jamdelion Jan 16, 2025
6408eaf
Merge branch 'main' into jh/experimental-readme-page
jamdelion Jan 16, 2025
f389f3c
Merge branch 'main' into jh/experimental-readme-page
jamdelion Jan 20, 2025
ad1fd62
Fix merge
jamdelion Jan 20, 2025
a9521ec
Add serviceLimitations column and get it working
jamdelion Jan 20, 2025
4d56ed4
Undo linting
jamdelion Jan 20, 2025
6fb504a
Undo linting again
jamdelion Jan 20, 2025
84f8b77
Use FlowTag work for labels
jamdelion Jan 20, 2025
7fde819
Add character counter and aria-describedBy plus refresh window on res…
jamdelion Jan 21, 2025
685aad6
Fix input spacing
jamdelion Jan 21, 2025
20ff912
Add clean-html process to demoUser
jamdelion Jan 21, 2025
945e8e7
Wording changes
jamdelion Jan 21, 2025
fff13fa
tidy up
jamdelion Jan 21, 2025
ee956c0
Add storybook file for ReadMePage
jamdelion Jan 21, 2025
94bdad2
Populate test.todos
jamdelion Jan 21, 2025
0f6ed23
Fix accessibility issue and populate two basic tests
jamdelion Jan 22, 2025
c36b93a
More test todos
jamdelion Jan 22, 2025
0e2e723
Display externalPortals in readmePage
jamdelion Jan 22, 2025
36f9dee
Update to latest planx-core
jamdelion Jan 22, 2025
4cba8ec
Install latest planx-core
jamdelion Jan 22, 2025
9218782
Point at main planx-core and use toggle on external portals
jamdelion Jan 23, 2025
b2bec23
Sort sentence casing
jamdelion Jan 23, 2025
46be914
Sort sentence casing
jamdelion Jan 23, 2025
92450d2
Merge branch 'main' into jh/experimental-readme-page
jamdelion Jan 23, 2025
16cc688
Fix editornav tests
jamdelion Jan 23, 2025
4678e0e
Make view only for teamViewers
jamdelion Jan 23, 2025
6d0e09f
Merge branch 'jh/experimental-readme-page' into jh/read-me-follow-ons
jamdelion Jan 23, 2025
7679a4c
Fix story error
jamdelion Jan 23, 2025
524cfb4
Separate out types file
jamdelion Jan 23, 2025
c82420e
Fix tests with user role mocked
jamdelion Jan 23, 2025
f80add9
Fix external portal test
jamdelion Jan 23, 2025
c2ac9b5
Add teamViewer test and extract test helpers
jamdelion Jan 23, 2025
552f611
Merge branch 'main' into jh/read-me-follow-ons
jamdelion Jan 28, 2025
c275865
Tidy up merge
jamdelion Jan 28, 2025
f56eaaf
Remove flowSlug prop
jamdelion Jan 28, 2025
d159f8b
Remove flow slug prop from more places
jamdelion Jan 29, 2025
2b600df
Replace flowslug to fix axe test
jamdelion Jan 29, 2025
d5afba4
Add fallback external portals message
jamdelion Jan 29, 2025
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
@@ -0,0 +1,26 @@
import { Meta, StoryObj } from "@storybook/react";

import { ReadMePage } from "./ReadMePage";

const meta = {
title: "Design System/Pages/ReadMe",
component: ReadMePage,
} satisfies Meta<typeof ReadMePage>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic = {
args: {
teamSlug: "barnet",
flowSlug: "Apply for prior permission",
flowInformation: {
status: "online",
description: "A long description of a service",
summary: "A short blurb",
limitations: "",
settings: {},
},
},
} satisfies Story;
66 changes: 44 additions & 22 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/ReadMePage.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@ import Typography from "@mui/material/Typography";
import { TextInputType } from "@planx/components/TextInput/model";
import { useFormik } from "formik";
import { useToast } from "hooks/useToast";
import React from "react";
import capitalize from "lodash/capitalize";
import React, { useState } from "react";
import FlowTag from "ui/editor/FlowTag/FlowTag";
import { FlowTagType, StatusVariant } from "ui/editor/FlowTag/types";
import InputGroup from "ui/editor/InputGroup";
@@ -16,25 +17,17 @@ import SettingsSection from "ui/editor/SettingsSection";
import { CharacterCounter } from "ui/shared/CharacterCounter";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";
import { object, string } from "yup";

import { ExternalPortals } from "../components/Sidebar/Search/ExternalPortalList/ExternalPortals";
import { useStore } from "../lib/store";
import { FlowInformation } from "../utils";

interface ReadMePageProps {
flowInformation: FlowInformation;
teamSlug: string;
}

interface ReadMePageForm {
serviceSummary: string;
serviceDescription: string;
serviceLimitations: string;
}
import { ReadMePageForm, ReadMePageProps } from "./types";

export const ReadMePage: React.FC<ReadMePageProps> = ({
flowInformation,
teamSlug,
flowSlug,
}) => {
const { status: flowStatus } = flowInformation;
const [
@@ -44,6 +37,7 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
updateFlowSummary,
flowLimitations,
updateFlowLimitations,
externalPortals,
flowName,
] = useStore((state) => [
state.flowDescription,
@@ -52,11 +46,16 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
state.updateFlowSummary,
state.flowLimitations,
state.updateFlowLimitations,
state.externalPortals,
state.flowName,
]);

const toast = useToast();

const hasExternalPortals = Boolean(Object.keys(externalPortals).length);

const [showExternalPortals, setShowExternalPortals] = useState(false);

const formik = useFormik<ReadMePageForm>({
initialValues: {
serviceSummary: flowSummary || "",
@@ -66,14 +65,14 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
onSubmit: async (values, { setSubmitting, setFieldError }) => {
try {
const updateFlowDescriptionPromise = updateFlowDescription(
values.serviceDescription
values.serviceDescription,
);
const updateFlowSummaryPromise = updateFlowSummary(
values.serviceSummary
values.serviceSummary,
);

const updateFlowLimitationsPromise = updateFlowLimitations(
values.serviceLimitations
values.serviceLimitations,
);

const [descriptionResult, summaryResult, limitationsResult] =
@@ -89,27 +88,27 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
if (!descriptionResult) {
setFieldError(
"serviceDescription",
"Unable to update the flow description. Please try again."
"Unable to update the flow description. Please try again.",
);
}
if (!summaryResult) {
setFieldError(
"serviceSummary",
"Unable to update the service summary. Please try again."
"Unable to update the service summary. Please try again.",
);
}
if (!limitationsResult) {
setFieldError(
"serviceLimitations",
"Unable to update the service limitations. Please try again."
"Unable to update the service limitations. Please try again.",
);
}
throw new Error("One or more updates failed");
}
} catch (error) {
console.error("Error updating descriptions:", error);
toast.error(
"An error occurred while updating descriptions. Please try again."
"An error occurred while updating descriptions. Please try again.",
);
} finally {
setSubmitting(false);
@@ -120,7 +119,7 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
validationSchema: object({
serviceSummary: string().max(
120,
"Service description must be 120 characters or less"
"Service description must be 120 characters or less",
),
}),
});
@@ -129,7 +128,8 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
<Container maxWidth="formWrap">
<SettingsSection>
<Typography variant="h2" component="h3" gutterBottom>
{flowName}
{/* fallback from request params if store not populated with flowName */}
{flowName || capitalize(flowSlug.replaceAll("-", " "))}
</Typography>

<Box display={"flex"}>
@@ -161,6 +161,7 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
disabled={!useStore.getState().canUserEditTeam(teamSlug)}
inputProps={{
"aria-describedby": "A short blurb on what this service is.",
"aria-label": "Service Description",
}}
/>
<CharacterCounter
@@ -235,6 +236,27 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
</Box>
</form>
</SettingsSection>
<Box pt={2}>
<Switch
label={"Show external portals"}
name={"service.status"}
variant="editorPage"
checked={showExternalPortals}
onChange={() => setShowExternalPortals(!showExternalPortals)}
/>
{showExternalPortals &&
(hasExternalPortals ? (
<Box pt={2} data-testid="searchExternalPortalList">
<InputLegend>External Portals</InputLegend>
<Typography variant="body1" my={2}>
Your service contains the following external portals:
</Typography>
<ExternalPortals externalPortals={externalPortals} />
</Box>
) : (
<Typography>This service has no external portals.</Typography>
))}
</Box>
</Container>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { act, screen } from "@testing-library/react";
import { FullStore, useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { setup } from "testUtils";
import { axe } from "vitest-axe";

import { ReadMePage } from "../ReadMePage";
import { defaultProps, longInput, platformAdminUser } from "./helpers";

const { getState, setState } = useStore;

let initialState: FullStore;

describe("Read Me Page component", () => {
beforeAll(() => (initialState = getState()));

beforeEach(() => {
getState().setUser(platformAdminUser);
});

afterEach(() => {
act(() => setState(initialState));
});

it("renders and submits data without an error", async () => {
const { user } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

await user.type(serviceSummaryInput, "a summary");

await user.click(screen.getByRole("button", { name: "Save" }));

expect(screen.getByText("a summary")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Reset changes" })); // refreshes page and refetches data

expect(getState().flowSummary).toEqual("a summary");
expect(screen.getByText("a summary")).toBeInTheDocument();
});

it("displays an error if the service description is longer than 120 characters", async () => {
const { user } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

await user.type(serviceSummaryInput, longInput);

expect(
await screen.findByText("You have 2 characters too many")
).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: "Save" }));

expect(
screen.getByText("Service description must be 120 characters or less")
).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: "Reset changes" })); // refreshes page and refetches data
expect(getState().flowSummary).toBe(""); // db has not been updated
});

it("displays data in the fields if there is already flow information in the database", async () => {
await act(async () =>
setState({
flowSummary: "This flow summary is in the db already",
})
);

setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(
screen.getByText("This flow summary is in the db already")
).toBeInTheDocument();
});

it.todo("displays an error toast if there is a server-side issue"); // waiting for PR 4019 to merge first so can use msw package

it("should not have any accessibility violations", async () => {
const { container } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

const results = await axe(container);
expect(results).toHaveNoViolations();
});

it("is not editable if the user has the teamViewer role", async () => {
const teamViewerUser = { ...platformAdminUser, isPlatformAdmin: false };
getState().setUser(teamViewerUser);

getState().setTeamMembers([{ ...teamViewerUser, role: "teamViewer" }]);

setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

expect(serviceSummaryInput).toBeDisabled();
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
});
});
26 changes: 26 additions & 0 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReadMePageProps } from "../types";

export const platformAdminUser = {
id: 1,
firstName: "Editor",
lastName: "Test",
isPlatformAdmin: true,
email: "[email protected]",
teams: [],
jwt: "x.y.z",
};

export const defaultProps = {
flowSlug: "apply-for-planning-permission",
teamSlug: "barnet",
flowInformation: {
status: "online",
description: "A long description of a service",
summary: "A short blurb",
limitations: "",
settings: {},
},
} as ReadMePageProps;

export const longInput =
"A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my who"; // 122 characters
13 changes: 13 additions & 0 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FlowInformation } from "../utils";

export interface ReadMePageProps {
flowInformation: FlowInformation;
teamSlug: string;
flowSlug: string;
}

export interface ReadMePageForm {
serviceSummary: string;
serviceDescription: string;
serviceLimitations: string;
}
Loading

Unchanged files with check annotations Beta

await page.getByText("Continue").click();
await page.getByLabel("email").fill(context.user.email);
await page.getByText("Continue").click();
await page.waitForLoadState("networkidle");

Check warning on line 97 in e2e/tests/ui-driven/src/invite-to-pay/agent.spec.ts

GitHub Actions / E2E tests

Unexpected use of networkidle
await expect(toggleInviteToPayButton).toBeDisabled();
});
context.flow?.slug
}/pay?analytics=false&paymentRequestId=INVALID-ID`;
await page.goto(invalidPaymentRequestURL);
await page.waitForLoadState("networkidle");

Check warning on line 98 in e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts

GitHub Actions / E2E tests

Unexpected use of networkidle
await expect(page.getByText(PAYMENT_NOT_FOUND_TEXT)).toBeVisible();
});
test("navigating to a URL without a paymentRequestId", async ({ page }) => {
const invalidPaymentRequestURL = `/${context.team!.slug!}/${context.flow?.slug}/pay?analytics=false`;
await page.goto(invalidPaymentRequestURL);
await page.waitForLoadState("networkidle");

Check warning on line 106 in e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts

GitHub Actions / E2E tests

Unexpected use of networkidle
await expect(page.getByText(PAYMENT_NOT_FOUND_TEXT)).toBeVisible();
});
) {
const paymentRequestURL = `/${context.team!.slug!}/${context.flow?.slug}/pay?analytics=false&paymentRequestId=${paymentRequest.id}`;
await page.goto(paymentRequestURL);
await page.waitForLoadState("networkidle");

Check warning on line 140 in e2e/tests/ui-driven/src/invite-to-pay/nominee.spec.ts

GitHub Actions / E2E tests

Unexpected use of networkidle
}
async function setupPaymentRequest(