Skip to content

Commit 1a855e9

Browse files
philreekshdrclaude
andauthored
feat(GAT-9204): Add shared access panel and tidy NHS SDE stepper states (#1604)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent afa7ad5 commit 1a855e9

10 files changed

Lines changed: 255 additions & 46 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { useTranslations } from "next-intl";
4+
import CohortDiscoveryButton from "@/components/CohortDiscoveryButton";
5+
import Paper from "@/components/Paper";
6+
7+
const TRANSLATION_PATH = "pages.account.profile.cohortDiscovery.stepper";
8+
9+
const CohortAccessPanel = () => {
10+
const t = useTranslations(TRANSLATION_PATH);
11+
12+
return (
13+
<Paper
14+
sx={{
15+
bgcolor: "white",
16+
height: "100%",
17+
display: "flex",
18+
alignItems: "center",
19+
justifyContent: "center",
20+
p: { mobile: 3, laptop: 4 },
21+
}}>
22+
<CohortDiscoveryButton label={t("accessButton")} />
23+
</Paper>
24+
);
25+
};
26+
27+
export default CohortAccessPanel;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import CohortAccessPanel from "./CohortAccessPanel";
2+
3+
export default CohortAccessPanel;

src/app/[locale]/account/profile/cohort-discovery-request/components/CohortAccessStepper/CohortAccessStepper.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,22 @@ describe("CohortAccessStepper", () => {
173173
expect(screen.queryByText("Application Review")).not.toBeInTheDocument();
174174
});
175175

176+
it("hides the access button when hideAccessButton is set", () => {
177+
mockUseCohortStatus.mockReturnValue({
178+
...baseStatus,
179+
requestStatus: "APPROVED",
180+
});
181+
182+
render(<CohortAccessStepper cmsContent={cmsContent} hideAccessButton />);
183+
184+
expect(screen.getByText("Approved")).toBeInTheDocument();
185+
expect(
186+
screen.queryByRole("button", {
187+
name: "Access Cohort Discovery tool",
188+
})
189+
).not.toBeInTheDocument();
190+
});
191+
176192
it("hides the steps while the status is still loading", () => {
177193
mockUseCohortStatus.mockReturnValue({
178194
...baseStatus,

src/app/[locale]/account/profile/cohort-discovery-request/components/CohortAccessStepper/CohortAccessStepper.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ const TERMS_HREF =
3636
interface CohortAccessStepperProps {
3737
cmsContent: templateRepeatFields;
3838
autoOpen?: boolean;
39+
hideAccessButton?: boolean;
3940
}
4041

4142
const CohortAccessStepper = ({
4243
cmsContent,
4344
autoOpen = false,
45+
hideAccessButton = false,
4446
}: CohortAccessStepperProps) => {
4547
const t = useTranslations(TRANSLATION_PATH);
4648
const tCd = useTranslations("pages.account.profile.cohortDiscovery");
@@ -168,7 +170,7 @@ const CohortAccessStepper = ({
168170
</Box>
169171
)}
170172
</Box>
171-
{!loading && isApproved && (
173+
{!loading && isApproved && !hideAccessButton && (
172174
<CohortDiscoveryButton
173175
label={t("accessButton")}
174176
wrapperSx={{ width: "auto" }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { templateRepeatFields } from "@/interfaces/Cms";
2+
import CohortDiscoveryCoverPage from "./CohortDiscoveryCoverPage";
3+
import { render, screen } from "@/utils/testUtils";
4+
5+
const mockUseCohortStatus = jest.fn();
6+
7+
jest.mock("@/hooks/useAuth", () => ({
8+
__esModule: true,
9+
default: () => ({ user: { id: 1 }, isLoading: false }),
10+
}));
11+
12+
jest.mock("@/hooks/useCohortStatus", () => ({
13+
__esModule: true,
14+
useCohortStatus: () => mockUseCohortStatus(),
15+
}));
16+
17+
jest.mock("../CohortAccessStepper", () => ({
18+
__esModule: true,
19+
default: ({ hideAccessButton }: { hideAccessButton?: boolean }) => (
20+
<div data-testid="cohort-access-stepper">
21+
{hideAccessButton ? "button-hidden" : "button-shown"}
22+
</div>
23+
),
24+
}));
25+
26+
jest.mock("../NhsSdeAccessStepper", () => ({
27+
__esModule: true,
28+
default: () => <div data-testid="nhs-sde-access-stepper" />,
29+
}));
30+
31+
jest.mock("@/components/CohortDiscoveryButton", () => ({
32+
__esModule: true,
33+
default: ({ label }: { label?: string }) => (
34+
<button type="button">{label ?? "Access"}</button>
35+
),
36+
}));
37+
38+
const baseStatus = {
39+
requestStatus: null,
40+
requestExpiry: null,
41+
nhseSdeRequestStatus: null,
42+
isLoading: false,
43+
hasFetched: true,
44+
refetch: jest.fn(),
45+
};
46+
47+
const cmsContent: templateRepeatFields = {
48+
title: "T",
49+
subTitle: "S",
50+
description: "D",
51+
contents: [],
52+
};
53+
54+
const renderPage = () =>
55+
render(<CohortDiscoveryCoverPage cmsContent={cmsContent} />);
56+
57+
describe("CohortDiscoveryCoverPage", () => {
58+
it("does not render a shared access panel when only one access is approved", () => {
59+
mockUseCohortStatus.mockReturnValue({
60+
...baseStatus,
61+
requestStatus: "APPROVED",
62+
nhseSdeRequestStatus: "IN PROCESS",
63+
});
64+
65+
renderPage();
66+
67+
expect(screen.getByText("button-shown")).toBeInTheDocument();
68+
expect(
69+
screen.queryByRole("button", {
70+
name: "Access Cohort Discovery tool",
71+
})
72+
).not.toBeInTheDocument();
73+
});
74+
75+
it("combines into a single shared access panel when both accesses are approved", () => {
76+
mockUseCohortStatus.mockReturnValue({
77+
...baseStatus,
78+
requestStatus: "APPROVED",
79+
nhseSdeRequestStatus: "APPROVED",
80+
});
81+
82+
renderPage();
83+
84+
expect(screen.getByText("button-hidden")).toBeInTheDocument();
85+
expect(
86+
screen.getByRole("button", {
87+
name: "Access Cohort Discovery tool",
88+
})
89+
).toBeInTheDocument();
90+
});
91+
});

src/app/[locale]/account/profile/cohort-discovery-request/components/CohortDiscoveryCoverPage/CohortDiscoveryCoverPage.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { useSearchParams } from "next/navigation";
77
import { templateRepeatFields } from "@/interfaces/Cms";
88
import Box from "@/components/Box";
99
import Container from "@/components/Container";
10+
import useAuth from "@/hooks/useAuth";
11+
import { useCohortStatus } from "@/hooks/useCohortStatus";
12+
import { COHORT_STATUS, NHS_SDE_STATUS } from "@/consts/cohortDiscovery";
13+
import CohortAccessPanel from "../CohortAccessPanel";
1014
import CohortAccessStepper from "../CohortAccessStepper";
1115
import NhsSdeAccessStepper from "../NhsSdeAccessStepper";
1216

@@ -23,6 +27,13 @@ export default function CohortDiscoveryCoverPage({
2327
return searchParams?.get("open") === "true";
2428
}, [searchParams]);
2529

30+
const { user } = useAuth();
31+
const { requestStatus, nhseSdeRequestStatus } = useCohortStatus(user?.id);
32+
33+
const bothApproved =
34+
requestStatus === COHORT_STATUS.APPROVED &&
35+
nhseSdeRequestStatus === NHS_SDE_STATUS.APPROVED;
36+
2637
return (
2738
<Container sx={{ display: "flex", flexDirection: "column" }}>
2839
<Box sx={{ bgcolor: "white", mb: 3, px: 4, pb: 1, pt: 3 }}>
@@ -35,15 +46,21 @@ export default function CohortDiscoveryCoverPage({
3546
columnSpacing={2}
3647
direction="row"
3748
alignItems="stretch">
38-
<Grid size={12}>
49+
<Grid
50+
size={bothApproved ? { mobile: 12, laptop: 8 } : 12}
51+
sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
3952
<CohortAccessStepper
4053
cmsContent={cmsContent}
4154
autoOpen={autoOpen}
55+
hideAccessButton={bothApproved}
4256
/>
43-
</Grid>
44-
<Grid size={12}>
4557
<NhsSdeAccessStepper />
4658
</Grid>
59+
{bothApproved && (
60+
<Grid size={{ mobile: 12, laptop: 4 }}>
61+
<CohortAccessPanel />
62+
</Grid>
63+
)}
4764
</Grid>
4865
</Container>
4966
);

src/app/[locale]/account/profile/cohort-discovery-request/components/NhsSdeAccessStepper/NhsSdeAccessStepper.test.tsx

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,26 +212,77 @@ describe("NhsSdeAccessStepper", () => {
212212
expect(screen.getByText("Approved")).toBeInTheDocument();
213213
});
214214

215-
it("shows a capitalised badge and no active-step actions when the NHS request is rejected", () => {
215+
it("hides all the steps once NHS access has been granted", () => {
216216
mockUseCohortStatus.mockReturnValue({
217217
...baseStatus,
218218
requestStatus: "APPROVED",
219-
nhseSdeRequestStatus: "REJECTED",
219+
nhseSdeRequestStatus: "APPROVED",
220220
});
221221

222222
renderStepper();
223223

224-
expect(screen.getByText("Rejected")).toBeInTheDocument();
225224
expect(
226-
screen.queryByRole("button", {
227-
name: "Open NHS SDE Registration Form",
228-
})
225+
screen.queryByText("Existing Cohort Discovery Access")
229226
).not.toBeInTheDocument();
230227
expect(
231-
screen.queryByRole("button", {
232-
name: "I confirm I have been approved by the NHS Research SDE",
233-
})
228+
screen.queryByText("Complete NHS SDE Registration Form")
234229
).not.toBeInTheDocument();
230+
expect(screen.queryByText("Access Decision")).not.toBeInTheDocument();
231+
});
232+
233+
it.each(["REJECTED", "BANNED", "SUSPENDED"])(
234+
"shows a capitalised badge and hides the steps when the NHS request is %s",
235+
status => {
236+
mockUseCohortStatus.mockReturnValue({
237+
...baseStatus,
238+
requestStatus: "APPROVED",
239+
nhseSdeRequestStatus: status,
240+
});
241+
242+
renderStepper();
243+
244+
expect(
245+
screen.getByText(
246+
status.charAt(0) + status.slice(1).toLowerCase()
247+
)
248+
).toBeInTheDocument();
249+
expect(
250+
screen.queryByText("Existing Cohort Discovery Access")
251+
).not.toBeInTheDocument();
252+
expect(
253+
screen.queryByText("Access Decision")
254+
).not.toBeInTheDocument();
255+
}
256+
);
257+
258+
it("lets an expired user restart the application from step 1", async () => {
259+
mockUseCohortStatus.mockReturnValue({
260+
...baseStatus,
261+
requestStatus: "APPROVED",
262+
nhseSdeRequestStatus: "EXPIRED",
263+
});
264+
265+
renderStepper();
266+
267+
expect(screen.getByText("Expired")).toBeInTheDocument();
268+
expect(
269+
screen.getByText("Existing Cohort Discovery Access")
270+
).toBeInTheDocument();
271+
272+
const applyButton = screen.getByRole("button", {
273+
name: "Apply for NHS Research SDE Cohort Data Access",
274+
});
275+
expect(applyButton).toBeInTheDocument();
276+
277+
await userEvent.click(applyButton);
278+
279+
expect(
280+
screen.getByRole("button", {
281+
name: "Open NHS SDE Registration Form",
282+
})
283+
).toBeInTheDocument();
284+
expect(screen.getByText("Awaiting Action")).toBeInTheDocument();
285+
expect(screen.queryByText("Expired")).not.toBeInTheDocument();
235286
});
236287

237288
it("reveals step 2 with the registration form once the user opts in from step 1", async () => {

0 commit comments

Comments
 (0)