Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit 7ea1e78

Browse files
authored
feat(button): add new experiment button (#184)
* feat(button): add new experiment button * feat(button): add mutation to create new exp * fix(title): change default experiment title to New Experiment * fix(cache): remove experiment field from the cache * feat(test): add tests for create new experiment
1 parent b63c4c2 commit 7ea1e78

File tree

11 files changed

+234
-13
lines changed

11 files changed

+234
-13
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { gql, useMutation } from "@apollo/client";
2+
3+
import { CREATE_EXPERIMENT_TYPE } from "types/globalTypes";
4+
5+
export const CREATE_EXPERIMENT = gql`
6+
mutation createExperiment($title: String!, $description: String!, $tags: [String!]! ) {
7+
createExperiment(createExperimentInput: { title: $title, description: $description, tags: $tags}) {
8+
uuid
9+
eid
10+
}
11+
}
12+
`;
13+
14+
export function useCreateExperiment() {
15+
const [mutate, { loading, data, error }] = useMutation<{
16+
createExperiment: CREATE_EXPERIMENT_TYPE;
17+
}>(CREATE_EXPERIMENT, {
18+
onError(error) {
19+
console.log("Create experiment failed", error);
20+
},
21+
});
22+
return { mutate, loading, data, error };
23+
}

aqueductcore/frontend/src/API/graphql/mutations/experiment/removeExperiment.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export function useRemoveExperiment() {
1212
const [mutate, { loading, data, error }] = useMutation<{
1313
removeExperiment: REMOVE_EXPERIMENT_TYPE;
1414
}>(REMOVE_EXPERIMENT, {
15+
update(cache) {
16+
cache.modify({
17+
fields: {
18+
experiment({ DELETE }) {
19+
return DELETE;
20+
},
21+
},
22+
})
23+
},
1524
onError(error) {
1625
console.log("Remove experiment failed", error);
1726
},

aqueductcore/frontend/src/__mocks__/AppContextAQDMock.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getExperimentFiles_mock } from "__mocks__/queries/experiment/getExperim
1919
import { getAllExperiments_mock } from "__mocks__/queries/experiment/getAllExperimentsMock";
2020
import { removeExperiment_mock } from "__mocks__/mutations/experiment/removeExperimentMock";
2121
import { updateExperiment_mock } from "__mocks__/mutations/experiment/updateExperimentMock";
22+
import { createExperiment_mock } from "__mocks__/mutations/experiment/createExperimentMock";
2223
import { getAllExtensions_mock } from "__mocks__/queries/extension/getAllExtensionsMock";
2324
import { getExperiment_mock } from "__mocks__/queries/experiment/getExperimentByIdMock";
2425
import { executeExtension_mock } from "__mocks__/mutations/extension/executeExtension";
@@ -40,6 +41,7 @@ interface AppContextAQDMockProps {
4041
getExperiment_mockMockMode?: keyof typeof getExperiment_mock;
4142
addTagToExperiment_mockMockMode?: keyof typeof addTagToExperiment_mock;
4243
removeTagFromExperiment_mockMockMode?: keyof typeof removeTagFromExperiment_mock;
44+
createExperiment_mockMockMode?: keyof typeof createExperiment_mock;
4345
//Users
4446
getUserInformation_mockMockMode?: keyof typeof getUserInformation_mock;
4547
//Extensions
@@ -66,6 +68,7 @@ function AppContextAQDMock({
6668
getExperiment_mockMockMode = "success",
6769
addTagToExperiment_mockMockMode = "success",
6870
removeTagFromExperiment_mockMockMode = "success",
71+
createExperiment_mockMockMode = "success",
6972
//Users
7073
getUserInformation_mockMockMode = "success",
7174
//Extensions
@@ -120,6 +123,7 @@ function AppContextAQDMock({
120123
...getExperiment_mock[getExperiment_mockMockMode],
121124
...addTagToExperiment_mock[addTagToExperiment_mockMockMode],
122125
...removeTagFromExperiment_mock[removeTagFromExperiment_mockMockMode],
126+
...createExperiment_mock[createExperiment_mockMockMode],
123127
// Users
124128
...getUserInformation_mock[getUserInformation_mockMockMode],
125129
//Extensions

aqueductcore/frontend/src/__mocks__/ExperimentsDataMock.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,15 @@ export const ExperimentsDataMock: ExperimentAllFieldsDataType[] = [
240240
},
241241
];
242242

243+
export const createdNewExperiment = {
244+
uuid: "020bbd7f-43a8-4f4e-996b-1cdaebc398dd",
245+
eid: "20221111-16",
246+
title: "New Experiment",
247+
description: "",
248+
tags: [],
249+
createdAt: "2022-11-23T00:00:00+00:00",
250+
createdBy: "admin",
251+
files: [],
252+
}
253+
243254
export const sample_eid = ExperimentsDataMock[0].eid;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CREATE_EXPERIMENT } from "API/graphql/mutations/experiment/createExperiment";
2+
import { createdNewExperiment } from "__mocks__/ExperimentsDataMock";
3+
4+
export const createExperiment_mock = {
5+
success: [
6+
{
7+
request: {
8+
query: CREATE_EXPERIMENT,
9+
variables: {
10+
title: createdNewExperiment.title,
11+
description: createdNewExperiment.description,
12+
tags: createdNewExperiment.tags
13+
}
14+
},
15+
result: {
16+
data: {
17+
createExperiment: {
18+
uuid: createdNewExperiment.uuid,
19+
eid: createdNewExperiment.eid
20+
}
21+
}
22+
}
23+
},
24+
],
25+
};

aqueductcore/frontend/src/__mocks__/queries/experiment/getExperimentByIdMock.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GET_EXPERIMENT_BY_ID } from "API/graphql/queries/experiment/getExperimentById";
2-
import { ExperimentsDataMock } from "__mocks__/ExperimentsDataMock";
2+
import { ExperimentsDataMock, createdNewExperiment } from "__mocks__/ExperimentsDataMock";
33

44
export const selected_experiment = ExperimentsDataMock[0];
55

@@ -60,6 +60,58 @@ export const getExperiment_mock = {
6060
},
6161
},
6262
maxUsageCount: Number.POSITIVE_INFINITY,
63+
},
64+
{
65+
request: {
66+
...request,
67+
variables: {
68+
experimentIdentifier: {
69+
type: 'EID',
70+
value: createdNewExperiment.eid
71+
},
72+
},
73+
},
74+
result: {
75+
data: {
76+
experiment: {
77+
uuid: createdNewExperiment.uuid,
78+
title: createdNewExperiment.title,
79+
description: createdNewExperiment.description,
80+
tags: createdNewExperiment.tags,
81+
eid: createdNewExperiment.eid,
82+
createdAt: createdNewExperiment.createdAt,
83+
createdBy: createdNewExperiment.createdBy,
84+
files: createdNewExperiment.files,
85+
},
86+
},
87+
},
88+
maxUsageCount: Number.POSITIVE_INFINITY,
89+
},
90+
{
91+
request: {
92+
...request,
93+
variables: {
94+
experimentIdentifier: {
95+
type: 'UUID',
96+
value: createdNewExperiment.uuid
97+
},
98+
},
99+
},
100+
result: {
101+
data: {
102+
experiment: {
103+
uuid: createdNewExperiment.uuid,
104+
title: createdNewExperiment.title,
105+
description: createdNewExperiment.description,
106+
tags: createdNewExperiment.tags,
107+
eid: createdNewExperiment.eid,
108+
createdAt: createdNewExperiment.createdAt,
109+
createdBy: createdNewExperiment.createdBy,
110+
files: createdNewExperiment.files,
111+
},
112+
},
113+
},
114+
maxUsageCount: Number.POSITIVE_INFINITY,
63115
}
64116
],
65117
};

aqueductcore/frontend/src/__tests__/ExperimentRecordsPage.test.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { act, render, waitFor } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
3+
import { Route, Routes } from "react-router-dom";
34

45
import { filterByThisTitle } from "__mocks__/queries/experiment/getAllExperimentsWithNameFilterMock";
56
import { filterByThisTag } from "__mocks__/queries/experiment/getAllExperimentsWithTagFilterMock";
67
import { ARCHIVED, experimentRecordsRowsPerPageOptions } from "constants/constants";
8+
import { ExperimentsDataMock, createdNewExperiment } from "__mocks__/ExperimentsDataMock";
79
import { ExperimentRecordsColumns } from "pages/ExperimentRecordsPage";
8-
import { ExperimentsDataMock } from "__mocks__/ExperimentsDataMock";
910
import ExperimentRecordsPage from "pages/ExperimentRecordsPage";
11+
import ExperimentDetailsPage from "pages/ExperimentDetailsPage";
1012
import AppContextAQDMock from "__mocks__/AppContextAQDMock";
1113

1214
// Experiment table
@@ -176,3 +178,50 @@ test("experiment table to keep the URL updated with filters - Search title", asy
176178
});
177179

178180
//todo: start and end date with the URL
181+
182+
// Create new experiment
183+
test("add new experiment button", () => {
184+
const { getByTitle } = render(
185+
<AppContextAQDMock getUserInformation_mockMockMode="viewOnlyAccess">
186+
<Routes>
187+
<Route path="/" element={<ExperimentRecordsPage />} />
188+
<Route path="/aqd/experiments/:experimentIdentifier" element={<ExperimentDetailsPage />} />
189+
</Routes>
190+
</AppContextAQDMock >)
191+
const newExp = getByTitle("Create New Experiment")
192+
expect(newExp).toBeInTheDocument()
193+
});
194+
195+
test("redirect to experiment detailed title being selected", async () => {
196+
const { getByTitle, findByText } = render(
197+
<AppContextAQDMock getUserInformation_mockMockMode="viewOnlyAccess">
198+
<Routes>
199+
<Route path="/" element={<ExperimentRecordsPage />} />
200+
<Route path="/aqd/experiments/:experimentIdentifier" element={<ExperimentDetailsPage />} />
201+
</Routes>
202+
</AppContextAQDMock >)
203+
204+
const newExp = getByTitle("Create New Experiment")
205+
userEvent.click(newExp)
206+
207+
//Right experiment's been chosen
208+
const eid = await findByText(createdNewExperiment.eid)
209+
expect(eid).toBeInTheDocument()
210+
});
211+
212+
test("title being selected when new experiment is created", async () => {
213+
const { getByTitle, findByTitle } = render(
214+
<AppContextAQDMock getUserInformation_mockMockMode="viewOnlyAccess">
215+
<Routes>
216+
<Route path="/" element={<ExperimentRecordsPage />} />
217+
<Route path="/aqd/experiments/:experimentIdentifier" element={<ExperimentDetailsPage />} />
218+
</Routes>
219+
</AppContextAQDMock >)
220+
221+
const newExp = getByTitle("Create New Experiment")
222+
userEvent.click(newExp)
223+
224+
//Title is focused
225+
const editExpTitle = findByTitle("Edit experiment title")
226+
expect(await editExpTitle).toHaveFocus()
227+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import AddBoxIcon from '@mui/icons-material/AddBox';
2+
import { IconButton, IconButtonProps } from '@mui/material';
3+
4+
export const logoWidth = 180;
5+
6+
export const AddButton = ({ ...props }: IconButtonProps) => {
7+
return (
8+
<IconButton size='large' {...props}>
9+
<AddBoxIcon fontSize='large' />
10+
</IconButton>
11+
);
12+
};

aqueductcore/frontend/src/components/molecules/ExperimentTitle/index.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button, ClickAwayListener, Grid, TextField, Typography, styled } from "@mui/material";
2-
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
32
import { ChangeEvent, useEffect, useRef, useState } from "react";
3+
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
4+
import { useLocation, useNavigate } from "react-router-dom";
45

56
import useDebouncedCallback from "hooks/useDebounceCallBack";
67
import { DEBOUNCE_DELAY } from "constants/constants";
@@ -16,8 +17,6 @@ const ExperimentName = styled(Typography)`
1617
line-height: ${(props) => `${props.theme.spacing(4)}`};
1718
`;
1819

19-
const ExperimentNameTitleField = styled(TextField)``;
20-
2120
interface ExperimentTitleProps {
2221
handleExperimentTitleUpdate: (value: string) => void;
2322
experimentTitle: string;
@@ -32,13 +31,14 @@ export function ExperimentTitleUpdate({
3231
const [editNameStatus, setEditTitleStatus] = useState(false);
3332
const [inputWidth, setInputWidth] = useState<string | number>("auto");
3433
const [internalExperimentTitle, setInternalExperimentTitle] = useState<string>(experimentTitle);
34+
const location = useLocation();
35+
const navigate = useNavigate()
3536

3637
const titleField = useRef<HTMLInputElement>(null);
3738
const debounced = useDebouncedCallback<string>(handleExperimentTitleUpdate, DEBOUNCE_DELAY);
38-
3939
const handleClickAway = () => setEditTitleStatus(false);
4040
const handleTitleUpdate = () => {
41-
setEditTitleStatus(!editNameStatus);
41+
setEditTitleStatus(true);
4242
setTimeout(() => {
4343
titleField.current?.focus();
4444
titleField.current?.setSelectionRange(
@@ -48,6 +48,16 @@ export function ExperimentTitleUpdate({
4848
}, 0);
4949
};
5050

51+
// Handle Just created experiment title being focused
52+
useEffect(() => {
53+
const isItJustCreated = location?.state && location.state?.from === 'create_new_exp';
54+
if (isItJustCreated) {
55+
handleTitleUpdate()
56+
}
57+
// remove location.state
58+
navigate(".", { replace: true });
59+
}, [])
60+
5161
useEffect(() => {
5262
if (titleField.current) {
5363
const textWidth = getTextWidth(internalExperimentTitle);
@@ -80,12 +90,13 @@ export function ExperimentTitleUpdate({
8090
<Grid item>
8191
{editNameStatus ? (
8292
<ClickAwayListener onClickAway={handleClickAway}>
83-
<ExperimentNameTitleField
93+
<TextField
8494
variant="standard"
8595
margin="none"
8696
value={internalExperimentTitle}
8797
fullWidth
8898
onChange={handleExperimentTitleUpdateInternal}
99+
onSubmit={handleClickAway}
89100
inputRef={titleField}
90101
sx={{
91102
py: 0,

aqueductcore/frontend/src/pages/ExperimentRecordsPage/index.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { LinearProgress, Typography, styled } from "@mui/material";
2-
import { useLocation, useSearchParams } from "react-router-dom";
1+
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
2+
import { LinearProgress, Stack, Typography, styled } from "@mui/material";
33
import Box from "@mui/material/Box";
44
import { useState } from "react";
55

66
import { useGetAllExperiments } from "API/graphql/queries/experiment/getAllExperiments";
7+
import { useCreateExperiment } from "API/graphql/mutations/experiment/createExperiment";
78
import { drawerTopOffset, mainPadding } from "components/templates/drawerLayout";
89
import ExperimentsListTable from "components/organisms/ExperimentsListTable";
910
import useFilterExperimentsByTag from "hooks/useFilterExperimentsByTag";
1011
import FilterExperiments from "components/organisms/FilterExperiments";
12+
import { AddButton } from "components/atoms/AddButton";
1113
import { useDidUpdateEffect } from "helper/functions";
1214
import { Error } from "components/atoms/Error";
1315
import {
@@ -203,7 +205,7 @@ function ExperimentRecordsPage({ category }: { category?: ExperimentRecordsPageT
203205
};
204206

205207
const emptyListErrorMessage = (pageUrl: string) => {
206-
switch(pageUrl) {
208+
switch (pageUrl) {
207209
case "/aqd/experiments/favourites":
208210
return "No favourited experiment records found";
209211
case "/aqd/experiments/archived":
@@ -221,13 +223,33 @@ function ExperimentRecordsPage({ category }: { category?: ExperimentRecordsPageT
221223
setRowsPerPage(experimentRecordsRowsPerPageOptions[0]);
222224
setPage(0);
223225
};
226+
const { mutate } = useCreateExperiment()
227+
const navigate = useNavigate()
228+
229+
const handleCreatecreatedNewExperiment = () => {
230+
mutate({
231+
variables: {
232+
title: 'New Experiment',
233+
description: '',
234+
tags: []
235+
},
236+
onCompleted(data) {
237+
navigate(`/aqd/experiments/${data.createExperiment.eid}`, {
238+
state: { from: 'create_new_exp' },
239+
})
240+
}
241+
})
242+
}
224243

225244
if (error) return <Error message={error.message} />;
226245
return (
227246
<Container>
228247
<Title>{handlePageName(location.pathname)}</Title>
229248
{/* //Guides would be added here */}
230-
<FilterExperiments filters={filters} setFilters={setFilters} handleResetPagination={handleResetPagination} />
249+
<Stack direction='row' justifyContent="space-between">
250+
<FilterExperiments filters={filters} setFilters={setFilters} handleResetPagination={handleResetPagination} />
251+
<AddButton title="Create New Experiment" onClick={handleCreatecreatedNewExperiment} />
252+
</Stack>
231253
<Box sx={{ mt: 2 }}>
232254
{loading ? <LinearProgress /> :
233255
processedExperimentData && pageInfo.count ? (
@@ -240,7 +262,7 @@ function ExperimentRecordsPage({ category }: { category?: ExperimentRecordsPageT
240262
experimentList={processedExperimentData}
241263
pageInfo={pageInfo}
242264
maxHeight={`calc(100vh - ${tableHeightOffset}px)`}
243-
/> ) :
265+
/>) :
244266
<NoExperimentsMessage>{emptyListErrorMessage(location.pathname)}</NoExperimentsMessage>
245267
}
246268
</Box>

0 commit comments

Comments
 (0)