Skip to content

Commit d5cae5c

Browse files
feat(frontend): support autofilling parameter values from search params
test(frontend): add tests for SingleTemplatePage in the dashboard
1 parent 53ed96e commit d5cae5c

File tree

18 files changed

+539
-27
lines changed

18 files changed

+539
-27
lines changed

frontend/dashboard/src/routes/SingleTemplatePage.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Suspense } from "react";
22
import { useParams, Link } from "react-router-dom";
33
import { Container, Box, Typography } from "@mui/material";
4-
import { Breadcrumbs, visitToText } from "@diamondlightsource/sci-react-ui";
4+
import { Breadcrumbs } from "@diamondlightsource/sci-react-ui";
55
import { WorkflowsNavbar } from "workflows-lib";
66
import { parseVisitAndTemplate } from "workflows-lib/lib/utils/commonUtils";
77
import TemplateViewRetrigger from "relay-workflows-lib/lib/views/TemplateViewRetrigger";
@@ -31,14 +31,6 @@ const SingleTemplatePage: React.FC = () => {
3131
mt={2}
3232
mb={10}
3333
>
34-
{workflowName && (
35-
<Typography align="center" mb={5}>
36-
Using initial parameters from{" "}
37-
<Link to={`/workflows/${visitToText(visit)}/${workflowName}`}>
38-
{workflowName}
39-
</Link>
40-
</Typography>
41-
)}
4234
{templateName && (
4335
<WorkflowErrorBoundaryWithRetry>
4436
{({ fetchKey }) => (
@@ -59,7 +51,7 @@ const SingleTemplatePage: React.FC = () => {
5951
visit={visit}
6052
/>
6153
) : (
62-
<TemplateView templateName={templateName} />
54+
<TemplateView templateName={templateName} visit={visit} />
6355
)}
6456
</Suspense>
6557
)}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import "@testing-library/jest-dom";
2+
import { render, screen } from "@testing-library/react";
3+
import SingleTemplatePage from "../src/routes/SingleTemplatePage";
4+
import { Visit } from "@diamondlightsource/sci-react-ui";
5+
import { MemoryRouter, Routes, Route } from "react-router-dom";
6+
7+
vi.mock("relay-workflows-lib/lib/views/TemplateView", () => ({
8+
default: ({
9+
templateName,
10+
visit,
11+
}: {
12+
templateName: string;
13+
visit: Visit;
14+
}) => (
15+
<p>
16+
TemplateView for {templateName} in visit {visit.proposalCode}
17+
{visit.proposalNumber}-{visit.number}
18+
</p>
19+
),
20+
}));
21+
22+
vi.mock("relay-workflows-lib/lib/views/TemplateViewRetrigger", () => ({
23+
default: ({
24+
templateName,
25+
workflowName,
26+
visit,
27+
}: {
28+
templateName: string;
29+
workflowName: string;
30+
visit: Visit;
31+
}) => (
32+
<p>
33+
TemplateViewRetrigger for {templateName} in visit {visit.proposalCode}
34+
{visit.proposalNumber}-{visit.number} using workflow {workflowName}
35+
</p>
36+
),
37+
}));
38+
39+
vi.mock("workflows-lib", async () => ({
40+
...(await vi.importActual("workflows-lib")),
41+
WorkflowsNavbar: () => <></>,
42+
}));
43+
44+
vi.mock("@diamondlightsource/sci-react-ui", async () => ({
45+
...(await vi.importActual("@diamondlightsource/sci-react-ui")),
46+
Breadcrumbs: () => <></>,
47+
}));
48+
49+
describe("SingleTemplatePage", () => {
50+
function renderWithPath(path: string) {
51+
render(
52+
<MemoryRouter initialEntries={[path]}>
53+
<Routes>
54+
<Route
55+
path="templates/:templateName/:prepopulate"
56+
element={<SingleTemplatePage />}
57+
/>
58+
</Routes>
59+
</MemoryRouter>,
60+
);
61+
}
62+
63+
it("does not render a view if no visit is provided", () => {
64+
render(
65+
<MemoryRouter initialEntries={["/templates/e02-mib2x"]}>
66+
<Routes>
67+
<Route
68+
path="templates/:templateName"
69+
element={<SingleTemplatePage />}
70+
/>
71+
</Routes>
72+
</MemoryRouter>,
73+
);
74+
expect(screen.queryAllByRole("text")).toHaveLength(0);
75+
});
76+
77+
it("renders a TemplateView when no workflow name is provided", () => {
78+
renderWithPath("/templates/e02-mib2x/mg36964-1");
79+
expect(screen.getByText(/TemplateView /i)).toBeInTheDocument();
80+
});
81+
82+
it("passes the visit from the address to the TemplateView", () => {
83+
renderWithPath("/templates/e02-mib2x/mg36964-1");
84+
expect(screen.getByText(/mg36964-1/i)).toBeInTheDocument();
85+
});
86+
87+
it("passes the template name from the address to the TemplateView", () => {
88+
renderWithPath("/templates/e02-mib2x/mg36964-1");
89+
expect(screen.getByText(/e02-mib2x/i)).toBeInTheDocument();
90+
});
91+
92+
it("renders a TemplateViewRetrigger when a workflow name is provided", () => {
93+
renderWithPath("/templates/e02-mib2x/mg36964-1-mock-workflow-1");
94+
expect(screen.getByText(/TemplateViewRetrigger/i)).toBeInTheDocument();
95+
});
96+
97+
it("passes the visit from the address to the TemplateViewRetrigger", () => {
98+
renderWithPath("/templates/e02-mib2x/mg36964-1-mock-workflow-1");
99+
expect(screen.getByText(/mg36964-1/i)).toBeInTheDocument();
100+
});
101+
102+
it("passes the template name from the address to the TemplateViewRetrigger", () => {
103+
renderWithPath("/templates/e02-mib2x/mg36964-1-mock-workflow-1");
104+
expect(screen.getByText(/e02-mib2x/i)).toBeInTheDocument();
105+
});
106+
107+
it("passes the workflow name from the address to the TemplateViewRetrigger", () => {
108+
renderWithPath("/templates/e02-mib2x/mg36964-1-mock-workflow-1");
109+
expect(screen.getByText(/workflow mock-workflow-1/i)).toBeInTheDocument();
110+
});
111+
});

frontend/dashboard/tsconfig.app.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"noUnusedLocals": true,
2020
"noUnusedParameters": true,
2121
"noFallthroughCasesInSwitch": true,
22-
"noUncheckedSideEffectImports": true
22+
"noUncheckedSideEffectImports": true,
23+
24+
"types": ["vitest/globals", "node"]
2325
},
24-
"include": ["src"]
26+
"include": ["src", "tests"]
2527
}

frontend/dashboard/tsconfig.node.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"noUnusedLocals": true,
1818
"noUnusedParameters": true,
1919
"noFallthroughCasesInSwitch": true,
20-
"noUncheckedSideEffectImports": true
20+
"noUncheckedSideEffectImports": true,
21+
22+
"types": ["vitest/globals"]
2123
},
22-
"include": ["vite.config.ts"]
24+
"include": ["vite.config.ts", "vitest.config.ts", "vitest.setup.ts"]
2325
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "vitest/config";
2+
import viteConfig from "./vite.config";
3+
4+
export default defineConfig({
5+
...viteConfig,
6+
mode: "test",
7+
test: {
8+
setupFiles: ["./vitest.setup.ts"],
9+
globals: true,
10+
environment: "jsdom",
11+
},
12+
});

frontend/dashboard/vitest.setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class ResizeObserverMock {
2+
observe() {}
3+
unobserve() {}
4+
disconnect() {}
5+
}
6+
7+
vi.stubGlobal("ResizeObserver", ResizeObserverMock);

frontend/relay-workflows-lib/lib/components/SubmissionForm.tsx

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useFragment } from "react-relay";
2-
import {
3-
JSONObject,
4-
SubmissionForm as SubmissionFormBase,
5-
Visit,
6-
} from "workflows-lib";
2+
import { SubmissionForm as SubmissionFormBase, Visit } from "workflows-lib";
73
import { JsonSchema, UISchemaElement } from "@jsonforms/core";
84
import { graphql } from "react-relay";
95
import { SubmissionFormFragment$key } from "./__generated__/SubmissionFormFragment.graphql";
106
import { SubmissionFormParametersFragment$key } from "./__generated__/SubmissionFormParametersFragment.graphql";
7+
import { Link, useSearchParams } from "react-router-dom";
8+
import { mergeParameters } from "../utils/workflowRelayUtils";
9+
import { Stack, Typography, useTheme } from "@mui/material";
10+
import { Info } from "@mui/icons-material";
11+
import { visitToText } from "@diamondlightsource/sci-react-ui";
1112

1213
export const SubmissionFormFragment = graphql`
1314
fragment SubmissionFormFragment on WorkflowTemplate {
@@ -32,29 +33,82 @@ const SubmissionForm = ({
3233
prepopulatedParameters,
3334
visit,
3435
onSubmit,
36+
workflowName,
3537
}: {
3638
template: SubmissionFormFragment$key;
3739
prepopulatedParameters?: SubmissionFormParametersFragment$key;
3840
visit?: Visit;
3941
onSubmit: (visit: Visit, parameters: object) => void;
42+
workflowName?: string;
4043
}) => {
4144
const data = useFragment(SubmissionFormFragment, template);
42-
const parameterData = useFragment(
45+
const reusedParameterData = useFragment(
4346
SubmissionFormParametersFragment,
4447
prepopulatedParameters,
4548
);
49+
const [searchParams] = useSearchParams();
50+
51+
const autofilledParameters = mergeParameters(
52+
reusedParameterData,
53+
searchParams,
54+
);
55+
56+
const parametersSchema = data.arguments as JsonSchema;
57+
58+
const overriddenKeys = Array.from(new Set(searchParams.keys())).filter(
59+
(key) => {
60+
return parametersSchema.properties && key in parametersSchema.properties;
61+
},
62+
);
63+
64+
const theme = useTheme();
65+
66+
const parameterMessage = (
67+
<>
68+
<Stack
69+
direction={"row"}
70+
alignItems={"flex-start"}
71+
display="inline-flex"
72+
spacing={theme.spacing(1)}
73+
>
74+
<Info fontSize="small" />
75+
<Typography>
76+
{workflowName && (
77+
<>
78+
Parameter values have been reused from{" "}
79+
<Link to={`/workflows/${visitToText(visit)}/${workflowName}`}>
80+
{workflowName}
81+
</Link>
82+
.{" "}
83+
</>
84+
)}
85+
{!!overriddenKeys.length && (
86+
<>
87+
The following parameters have been overwritten by values from the
88+
URL link: '{overriddenKeys.join("', '")}'
89+
</>
90+
)}
91+
</Typography>
92+
</Stack>
93+
</>
94+
);
4695

4796
return (
4897
<SubmissionFormBase
4998
title={data.title ?? data.name}
5099
maintainer={data.maintainer}
51100
repository={data.repository}
52101
description={data.description ?? undefined}
53-
parametersSchema={data.arguments as JsonSchema}
102+
parametersSchema={parametersSchema}
54103
parametersUISchema={data.uiSchema as UISchemaElement}
55104
visit={visit}
56-
prepopulatedParameters={parameterData?.parameters as JSONObject}
105+
prepopulatedParameters={autofilledParameters}
57106
onSubmit={onSubmit}
107+
parameterMessage={
108+
overriddenKeys.length || reusedParameterData
109+
? parameterMessage
110+
: undefined
111+
}
58112
/>
59113
);
60114
};

frontend/relay-workflows-lib/lib/utils/workflowRelayUtils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { isWorkflowWithTasks } from "../utils/coreUtils";
55
import { useFragment } from "react-relay";
66
import { WorkflowTasksFragment } from "../graphql/WorkflowTasksFragment";
77
import { WorkflowTasksFragment$key } from "../graphql/__generated__/WorkflowTasksFragment.graphql";
8+
import { JSONObject } from "workflows-lib";
9+
import { SubmissionFormParametersFragment$data } from "../components/__generated__/SubmissionFormParametersFragment.graphql";
810

911
export function updateSearchParamsWithTaskIds(
1012
updatedTaskIds: string[],
@@ -81,3 +83,14 @@ export function useFetchedTasks(
8183

8284
return fetchedTasks;
8385
}
86+
87+
export function mergeParameters(
88+
reusedParameterData: SubmissionFormParametersFragment$data | null | undefined,
89+
searchParams: URLSearchParams,
90+
) {
91+
const searchParameterData = Object.fromEntries(searchParams.entries());
92+
return {
93+
...reusedParameterData?.parameters,
94+
...searchParameterData,
95+
} as JSONObject;
96+
}

frontend/relay-workflows-lib/lib/views/TemplateView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ export default function TemplateView({
3939
templateName,
4040
visit,
4141
prepopulatedParameters,
42+
workflowName,
4243
}: {
4344
templateName: string;
4445
visit?: Visit;
4546
prepopulatedParameters?: SubmissionFormParametersFragment$key;
47+
workflowName?: string;
4648
}) {
4749
const storedVisit = visitTextToVisit(
4850
localStorage.getItem("instrumentSessionID") ?? "",
@@ -95,6 +97,7 @@ export default function TemplateView({
9597
prepopulatedParameters={prepopulatedParameters}
9698
visit={visit ?? storedVisit ?? undefined}
9799
onSubmit={submitWorkflow}
100+
workflowName={workflowName}
98101
/>
99102
<Box sx={{ width: { xs: "100%", sm: "100%", md: "800px" } }}>
100103
<SubmittedMessagesList submittedData={submissionData} />

frontend/relay-workflows-lib/lib/views/TemplateViewRetrigger.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Visit } from "workflows-lib";
44
import { TemplateViewRetriggerQuery as TemplateViewRetriggerQueryType } from "./__generated__/TemplateViewRetriggerQuery.graphql";
55
import TemplateView from "./TemplateView";
66

7-
const TemplateViewRetriggerQuery = graphql`
7+
export const TemplateViewRetriggerQuery = graphql`
88
query TemplateViewRetriggerQuery(
99
$visit: VisitInput!
1010
$workflowName: String!
@@ -37,6 +37,7 @@ export default function TemplateViewWithRetrigger({
3737
templateName={templateName}
3838
visit={visit}
3939
prepopulatedParameters={retriggerData.workflow}
40+
workflowName={workflowName}
4041
/>
4142
);
4243
}

0 commit comments

Comments
 (0)