From 645758065d74804016c689826cfe18dc374d007d Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 25 May 2023 09:22:32 +0100 Subject: [PATCH 01/68] #1529 - add generate DOI button that checks ismintable --- .../public/res/default.json | 3 ++ .../src/ConfigProvider.tsx | 2 + .../datagateway-download/src/downloadApi.ts | 40 ++++++++++++++ .../src/downloadApiHooks.ts | 53 ++++++++++++++++++- .../downloadCartTable.component.tsx | 35 +++++++++++- 5 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/datagateway-download/public/res/default.json b/packages/datagateway-download/public/res/default.json index 4407133bf..5f3fd73ac 100644 --- a/packages/datagateway-download/public/res/default.json +++ b/packages/datagateway-download/public/res/default.json @@ -85,6 +85,9 @@ "remove": "Remove {{name}} from selection", "remove_all": "Remove All", "download": "Download Selection", + "generate_DOI": "Generate DOI", + "mintability_loading": "Checking whether you have permission to mint these files...", + "not_mintable": "You do not have permission to mint these files", "number_of_files": "Number of files", "total_size": "Total size", "no_selections": "No data selected. <2>Browse or <6>search for data.", diff --git a/packages/datagateway-download/src/ConfigProvider.tsx b/packages/datagateway-download/src/ConfigProvider.tsx index d4881e22a..befd17870 100644 --- a/packages/datagateway-download/src/ConfigProvider.tsx +++ b/packages/datagateway-download/src/ConfigProvider.tsx @@ -15,6 +15,7 @@ export interface DownloadSettings { apiUrl: string; downloadApiUrl: string; idsUrl: string; + doiMinterUrl?: string; fileCountMax?: number; totalSizeMax?: number; @@ -40,6 +41,7 @@ const initialConfiguration: DownloadSettings = { apiUrl: '', downloadApiUrl: '', idsUrl: '', + doiMinterUrl: '', fileCountMax: undefined, totalSizeMax: undefined, accessMethods: {}, diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 0da76c37b..bc0122ee7 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -389,3 +389,43 @@ export const getPercentageComplete = async ({ const isStatus = Number.isNaN(maybeNumber); return isStatus ? data : maybeNumber; }; + +/** + * Returns true if a user is able to mint a DOI for their cart, otherwise false + */ +export const isCartMintable = async ( + cart: DownloadCartItem[], + doiMinterUrl: string +): Promise => { + const investigations: number[] = []; + const datasets: number[] = []; + const datafiles: number[] = []; + cart.forEach((cartItem) => { + if (cartItem.entityType === 'investigation') + investigations.push(cartItem.entityId); + if (cartItem.entityType === 'dataset') datasets.push(cartItem.entityId); + if (cartItem.entityType === 'datafile') datafiles.push(cartItem.entityId); + }); + const { status } = await axios + .post( + `${doiMinterUrl}/ismintable`, + { + investigations: { ids: investigations }, + datasets: { ids: datasets }, + datafiles: { ids: datafiles }, + }, + { + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, + }, + } + ) + .catch((error) => { + // catch 403 error as it's not really an error! + if (axios.isAxiosError(error) && error.response?.status === 403) + return error.response; + throw error; + }); + + return status === 200; +}; diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 1815ad4ba..b1078ed4d 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -1,5 +1,9 @@ import { AxiosError } from 'axios'; -import type { Download, DownloadStatus } from 'datagateway-common'; +import { + Download, + DownloadStatus, + InvalidateTokenType, +} from 'datagateway-common'; import { DownloadCartItem, fetchDownloadCart, @@ -8,6 +12,7 @@ import { NotificationType, retryICATErrors, } from 'datagateway-common'; +import log from 'loglevel'; import pLimit from 'p-limit'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,9 +30,10 @@ import { UseQueryResult, } from 'react-query'; import { DownloadSettingsContext } from './ConfigProvider'; -import type { +import { DownloadProgress, DownloadTypeStatus, + isCartMintable, SubmitCartZipType, } from './downloadApi'; import { @@ -772,3 +778,46 @@ export const useDownloadPercentageComplete = ({ } ); }; + +/** + * Queries whether a cart is mintable. + * @param download The {@link Download} that this query should query the restore progress of. + */ +export const useIsCartMintable = ( + cart?: DownloadCartItem[] +): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + const { doiMinterUrl } = settings; + + return useQuery( + ['ismintable', cart], + () => { + if (doiMinterUrl && cart && cart.length > 1) + return isCartMintable(cart, doiMinterUrl); + else return Promise.resolve(false); + }, + { + onError: (error) => { + log.error(error); + if (error.response?.status === 401) { + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: InvalidateTokenType, + payload: { + severity: 'error', + message: + localStorage.getItem('autoLogin') === 'true' + ? 'Your session has expired, please reload the page' + : 'Your session has expired, please login again', + }, + }, + }) + ); + } + }, + staleTime: Infinity, + enabled: typeof doiMinterUrl !== 'undefined', + } + ); +}; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index 4c389d298..ec23539cf 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -9,6 +9,7 @@ import { Link, Paper, Theme, + Tooltip, Typography, } from '@mui/material'; import { @@ -29,6 +30,7 @@ import { DownloadSettingsContext } from '../ConfigProvider'; import { useCart, useDatafileCounts, + useIsCartMintable, useIsTwoLevel, useRemoveAllFromCart, useRemoveEntityFromCart, @@ -61,6 +63,8 @@ const DownloadCartTable: React.FC = ( const { mutate: removeAllDownloadCartItems, isLoading: removingAll } = useRemoveAllFromCart(); const { data, isFetching: dataLoading } = useCart(); + const { data: mintable, isLoading: cartMintabilityLoading } = + useIsCartMintable(data); const fileCountQueries = useDatafileCounts(data); const sizeQueries = useSizes(data); @@ -477,7 +481,7 @@ const DownloadCartTable: React.FC = ( + {settings.doiMinterUrl && ( + + + {/* need this span so the tooltip works when the button is disabled */} + + + + + + )} + + ); +}; + +export default AcceptDataPolicy; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index ec23539cf..e053c3b2b 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -509,9 +509,8 @@ const DownloadCartTable: React.FC = ( variant="contained" color="primary" disabled={cartMintabilityLoading || !mintable} - onClick={() => { - // TODO: mint doi - }} + component={RouterLink} + to="/download/mint" > {t('downloadCart.generate_DOI')} From 63b2a43136f5ed8d6194da810492182724a6f230 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 26 May 2023 11:53:51 +0100 Subject: [PATCH 03/68] #1531 - fetch list of users for every item in the cart --- .../DOIGenerationForm.component.tsx | 9 ++ .../datagateway-download/src/downloadApi.ts | 91 +++++++++++++++++++ .../src/downloadApiHooks.ts | 23 ++++- 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index c6bf2a733..3479b612f 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -1,5 +1,6 @@ import { Box, Grid, Paper, TextField, Typography } from '@mui/material'; import React from 'react'; +import { useCart, useCartUsers } from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; type DOIGenerationFormProps = { @@ -9,6 +10,9 @@ type DOIGenerationFormProps = { const DOIGenerationForm: React.FC = (props) => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(true); + const { data: cart } = useCart(); + const { data: users } = useCartUsers(cart); + return ( {acceptedDataPolicy ? ( @@ -51,6 +55,11 @@ const DOIGenerationForm: React.FC = (props) => { Creators + {users?.map((user, index) => ( + + {JSON.stringify(user)} + + ))} diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index bc0122ee7..ee807e487 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -1,10 +1,13 @@ import axios from 'axios'; import type { Datafile, + Dataset, Download, DownloadCart, DownloadCartItem, + Investigation, SubmitCart, + User, } from 'datagateway-common'; import { readSciGatewayToken } from 'datagateway-common'; import type { DownloadSettings } from './ConfigProvider'; @@ -429,3 +432,91 @@ export const isCartMintable = async ( return status === 200; }; + +const fetchEntityUsers = ( + apiUrl: string, + entityId: number, + entityType: 'investigation' | 'dataset' | 'datafile' +): Promise => { + const params = new URLSearchParams(); + params.append('where', JSON.stringify({ id: { eq: entityId } })); + + if (entityType === 'investigation') + params.append('include', JSON.stringify({ investigationUsers: 'user' })); + if (entityType === 'dataset') + params.append( + 'include', + JSON.stringify({ investigation: { investigationUsers: 'user' } }) + ); + if (entityType === 'datafile') + params.append( + 'include', + JSON.stringify({ + dataset: { investigation: { investigationUsers: 'user' } }, + }) + ); + + return axios + .get(`${apiUrl}/${entityType}s`, { + params, + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, + }, + }) + .then((response) => { + const entity = response.data[0]; + if (entityType === 'investigation') { + return (entity as Investigation).investigationUsers?.map( + (iUser) => iUser.user + ) as User[]; + } + if (entityType === 'dataset') { + return (entity as Dataset).investigation?.investigationUsers?.map( + (iUser) => iUser.user + ) as User[]; + } + if (entityType === 'datafile') + return ( + entity as Datafile + ).dataset?.investigation?.investigationUsers?.map( + (iUser) => iUser.user + ) as User[]; + return []; + }); +}; + +/** + * Deduplicates items in an array + * @param array Array to make unique + * @param key Function to apply to an array item that returns a primitive that keys that item + * @returns a deduplicated array + */ +function uniqBy(array: T[], key: (item: T) => number | string): T[] { + const seen: Record = {}; + return array.filter(function (item) { + const k = key(item); + return seen.hasOwnProperty(k) ? false : (seen[k] = true); + }); +} + +/** + * Returns a list of users from ICAT which are InvestigationUsers for each item in the cart + */ +export const getCartUsers = async ( + cart: DownloadCartItem[], + settings: Pick +): Promise => { + let users: User[] = []; + for (const cartItem of cart) { + const entityUsers = await fetchEntityUsers( + settings.apiUrl, + cartItem.entityId, + cartItem.entityType + ); + users = users.concat(entityUsers); + } + + users = uniqBy(users, (item) => item.id); + + return users; +}; diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index b1078ed4d..0c64adf66 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -3,6 +3,7 @@ import { Download, DownloadStatus, InvalidateTokenType, + User, } from 'datagateway-common'; import { DownloadCartItem, @@ -33,6 +34,7 @@ import { DownloadSettingsContext } from './ConfigProvider'; import { DownloadProgress, DownloadTypeStatus, + getCartUsers, isCartMintable, SubmitCartZipType, } from './downloadApi'; @@ -781,7 +783,7 @@ export const useDownloadPercentageComplete = ({ /** * Queries whether a cart is mintable. - * @param download The {@link Download} that this query should query the restore progress of. + * @param cart The {@link Cart} that is checked */ export const useIsCartMintable = ( cart?: DownloadCartItem[] @@ -821,3 +823,22 @@ export const useIsCartMintable = ( } ); }; + +/** + * Gets the total list of users associated with each item in the cart + * @param cart The {@link Cart} that we're getting the users for + */ +export const useCartUsers = ( + cart?: DownloadCartItem[] +): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + + return useQuery( + ['cartUsers', cart], + () => getCartUsers(cart ?? [], settings), + { + onError: handleICATError, + staleTime: Infinity, + } + ); +}; From 30d8b3ca54e2c0a566df7d02a89decf4c32275cb Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 26 May 2023 15:09:29 +0100 Subject: [PATCH 04/68] #1531 - add selected user table & allow for deletion of users other than the current user --- packages/datagateway-common/src/app.types.tsx | 2 + .../DOIGenerationForm.component.tsx | 61 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index e3f0f1216..ad62dc996 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -96,6 +96,8 @@ export interface User { id: number; name: string; fullName?: string; + email?: string; + affiliation?: string; } export interface Sample { diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 3479b612f..3eedd4cdc 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -1,4 +1,17 @@ -import { Box, Grid, Paper, TextField, Typography } from '@mui/material'; +import { + Box, + Button, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; import { useCart, useCartUsers } from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; @@ -9,10 +22,15 @@ type DOIGenerationFormProps = { const DOIGenerationForm: React.FC = (props) => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(true); + const [selectedUsers, setSelectedUsers] = React.useState([]); const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); + React.useEffect(() => { + if (users) setSelectedUsers(users); + }, [users]); + return ( {acceptedDataPolicy ? ( @@ -55,11 +73,42 @@ const DOIGenerationForm: React.FC = (props) => { Creators - {users?.map((user, index) => ( - - {JSON.stringify(user)} - - ))} + + + + Name + Affiliation + Email + Action + + + + {selectedUsers?.map((user) => ( + + {user.fullName} + {user?.affiliation} + {user?.email} + + + + + ))} + +
From f8fb7b8ac0b0b08c9aa034a65fdf839576c37594 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 26 May 2023 15:52:19 +0100 Subject: [PATCH 05/68] #1531 - improve acceptDataPolicy look --- .../acceptDataPolicy.component.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx index f3d1303db..ed4e599b9 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx @@ -1,4 +1,4 @@ -import { Button } from '@mui/material'; +import { Button, Grid, Paper, Typography } from '@mui/material'; import React from 'react'; type AcceptDataPolicyProps = { @@ -7,10 +7,38 @@ type AcceptDataPolicyProps = { const AcceptDataPolicy: React.FC = (props) => { return ( -
- Accept data policy{' '} - -
+ + + + + + Accept data policy wrghwurgh ergue rgeyrg erug er geurygerygueyrg + e rgeuygeurguerg rg ergyerg erg erg erg erg ergyeurgyuerg er + gerygueryg e rgeryguyerigy ergi er gery gergu i fg ewuefhuwef wef + wef weufw eufw ef wef wefiw efiwue f wefyweufy wfe wefy wfe + wiefwuiefi wue fw feiuwe fuwye uiewy fiwey fwy fewiufy iwyiefyiwue + f + + + + + + + + ); }; From cdd334acf8ab1cdbe94c0f4a6304cd22ae5fd9a9 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 1 Jun 2023 11:05:26 +0100 Subject: [PATCH 06/68] #1531 - implement add users ability I've mocked the backend for now - might need to adjust the querying code when it gets implemented --- .../DOIGenerationForm.component.tsx | 148 +++++++++++++----- .../datagateway-download/src/downloadApi.ts | 35 ++++- .../src/downloadApiHooks.ts | 48 ++++++ 3 files changed, 190 insertions(+), 41 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 3eedd4cdc..1c9f7871f 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -13,7 +13,7 @@ import { } from '@mui/material'; import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; -import { useCart, useCartUsers } from '../downloadApiHooks'; +import { useCart, useCartUsers, useCheckUser } from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; type DOIGenerationFormProps = { @@ -23,9 +23,12 @@ type DOIGenerationFormProps = { const DOIGenerationForm: React.FC = (props) => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(true); const [selectedUsers, setSelectedUsers] = React.useState([]); + const [email, setEmail] = React.useState(''); + const [emailError, setEmailError] = React.useState(''); const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); + const { refetch: checkUser } = useCheckUser(email); React.useEffect(() => { if (users) setSelectedUsers(users); @@ -70,45 +73,110 @@ const DOIGenerationForm: React.FC = (props) => { elevation={0} variant="outlined" > - - Creators - - - - - Name - Affiliation - Email - Action - - - - {selectedUsers?.map((user) => ( - - {user.fullName} - {user?.affiliation} - {user?.email} - - - - - ))} - -
+ + + + Creators + + + 0 ? 2 : 0 }} + > + + 0} + helperText={emailError.length > 0 ? emailError : ''} + sx={{ + // this CSS makes it so that the helperText doesn't mess with the button alignment + '& .MuiFormHelperText-root': { + position: 'absolute', + bottom: '-1.5rem', + }, + }} + value={email} + onChange={(event) => setEmail(event.target.value)} + /> + + + + + + + + + + Name + Affiliation + Email + Action + + + + {selectedUsers?.map((user) => ( + + {user.fullName} + {user?.affiliation} + {user?.email} + + + + + ))} + +
+
+
diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index ee807e487..e2348019a 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import type { Datafile, Dataset, @@ -520,3 +520,36 @@ export const getCartUsers = async ( return users; }; + +/** + * Sends an email to the API and it checks if it's a valid ICAT User, on success + * it returns the User, on failure it returns 404 + */ +export const checkUser = ( + email: string, + settings: Pick +): Promise => { + return Promise.resolve({ + email: 'test@example.com', + id: 1, + name: 'Test', + }); + // return Promise.reject( + // new AxiosError('test message', '404', undefined, undefined, { + // status: 404, + // statusText: 'Not found', + // data: 'test 2', + // headers: {}, + // config: {}, + // }) + // ); + return axios + .get(`${settings.doiMinterUrl}/user/${email}`, { + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, + }, + }) + .then((response) => { + return response.data; + }); +}; diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 0c64adf66..593bbffd6 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -32,6 +32,7 @@ import { } from 'react-query'; import { DownloadSettingsContext } from './ConfigProvider'; import { + checkUser, DownloadProgress, DownloadTypeStatus, getCartUsers, @@ -842,3 +843,50 @@ export const useCartUsers = ( } ); }; + +/** + * Checks whether an email belongs to an ICAT User + * @param email The email that we're checking + * @returns the {@link User} that matches the email, or 404 + */ +export const useCheckUser = ( + email: string +): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + + return useQuery(['checkUser', email], () => checkUser(email, settings), { + onError: (error) => { + log.error(error); + if (error.response?.status === 401) { + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: InvalidateTokenType, + payload: { + severity: 'error', + message: + localStorage.getItem('autoLogin') === 'true' + ? 'Your session has expired, please reload the page' + : 'Your session has expired, please login again', + }, + }, + }) + ); + } + }, + retry: (failureCount: number, error: AxiosError) => { + if ( + // user not logged in, error code will log them out + error.response?.status === 401 || + // email doesn't match user - don't retry as this is a correct response from the server + error.response?.status === 404 || + failureCount >= 3 + ) + return false; + return true; + }, + // set enabled false to only fetch on demand when the add creator button is pressed + enabled: false, + cacheTime: 0, + }); +}; From ee6f393889a47c06d8b35473cd11dca4c689d6ea Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 2 Jun 2023 11:12:05 +0100 Subject: [PATCH 07/68] #1531 - connect check user functionality to backend properly also do some prep for generate DOI functionality --- .../DOIGenerationForm.component.tsx | 58 +++++++++++++++---- .../datagateway-download/src/downloadApi.ts | 14 ----- .../src/downloadApiHooks.ts | 2 + 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 1c9f7871f..8b8674dcf 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -11,6 +11,7 @@ import { TextField, Typography, } from '@mui/material'; +import { AxiosError } from 'axios'; import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; import { useCart, useCartUsers, useCheckUser } from '../downloadApiHooks'; @@ -25,6 +26,8 @@ const DOIGenerationForm: React.FC = (props) => { const [selectedUsers, setSelectedUsers] = React.useState([]); const [email, setEmail] = React.useState(''); const [emailError, setEmailError] = React.useState(''); + const [title, setTitle] = React.useState(''); + const [description, setDescription] = React.useState(''); const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); @@ -50,7 +53,13 @@ const DOIGenerationForm: React.FC = (props) => { - + setTitle(event.target.value)} + /> = (props) => { multiline rows={4} fullWidth + value={description} + onChange={(event) => setDescription(event.target.value)} /> @@ -94,6 +105,7 @@ const DOIGenerationForm: React.FC = (props) => { error={emailError.length > 0} helperText={emailError.length > 0 ? emailError : ''} sx={{ + backgroundColor: 'background.default', // this CSS makes it so that the helperText doesn't mess with the button alignment '& .MuiFormHelperText-root': { position: 'absolute', @@ -101,7 +113,10 @@ const DOIGenerationForm: React.FC = (props) => { }, }} value={email} - onChange={(event) => setEmail(event.target.value)} + onChange={(event) => { + setEmail(event.target.value); + setEmailError(''); + }} /> @@ -112,9 +127,9 @@ const DOIGenerationForm: React.FC = (props) => { selectedUsers.every( (selectedUser) => selectedUser.email !== email ) - ) - checkUser({ throwOnError: true }).then( - (response) => { + ) { + checkUser({ throwOnError: true }) + .then((response) => { // add user const user = response.data; if (user) { @@ -123,14 +138,29 @@ const DOIGenerationForm: React.FC = (props) => { user, ]); setEmail(''); - } else { + } + }) + .catch( + ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { // TODO: check this is the right message from the API setEmailError( - response.error?.message ?? 'Error' + error.response?.data?.detail + ? typeof error.response.data + .detail === 'string' + ? error.response.data.detail + : error.response.data.detail[0].msg + : 'Error' ); } - } - ); + ); + } else { + setEmailError('Cannot add duplicate user'); + setEmail(''); + } }} > Add Creator @@ -138,7 +168,12 @@ const DOIGenerationForm: React.FC = (props) => { - +
Name @@ -179,6 +214,9 @@ const DOIGenerationForm: React.FC = (props) => { + + + diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index e2348019a..140227e14 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -529,20 +529,6 @@ export const checkUser = ( email: string, settings: Pick ): Promise => { - return Promise.resolve({ - email: 'test@example.com', - id: 1, - name: 'Test', - }); - // return Promise.reject( - // new AxiosError('test message', '404', undefined, undefined, { - // status: 404, - // statusText: 'Not found', - // data: 'test 2', - // headers: {}, - // config: {}, - // }) - // ); return axios .get(`${settings.doiMinterUrl}/user/${email}`, { headers: { diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 593bbffd6..69e6b716c 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -880,6 +880,8 @@ export const useCheckUser = ( error.response?.status === 401 || // email doesn't match user - don't retry as this is a correct response from the server error.response?.status === 404 || + // email is invalid - don't retry as this is correct response from the server + error.response?.status === 422 || failureCount >= 3 ) return false; From 962a19c9293ddbbd7dedecd80a44a569b20c7861 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 2 Jun 2023 11:58:20 +0100 Subject: [PATCH 08/68] #1531 - add simple summary table also add loading spinner to user table and disable generate DOI button when requirements aren't met --- .../DOIGenerationForm.component.tsx | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 8b8674dcf..20143501d 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -1,13 +1,16 @@ import { Box, Button, + CircularProgress, Grid, Paper, + Tab, Table, TableBody, TableCell, TableHead, TableRow, + Tabs, TextField, Typography, } from '@mui/material'; @@ -28,6 +31,16 @@ const DOIGenerationForm: React.FC = (props) => { const [emailError, setEmailError] = React.useState(''); const [title, setTitle] = React.useState(''); const [description, setDescription] = React.useState(''); + const [currentTab, setCurrentTab] = React.useState< + 'investigation' | 'dataset' | 'datafile' + >('investigation'); + + const handleTabChange = ( + event: React.SyntheticEvent, + newValue: 'investigation' | 'dataset' | 'datafile' + ): void => { + setCurrentTab(newValue); + }; const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); @@ -183,7 +196,17 @@ const DOIGenerationForm: React.FC = (props) => { - {selectedUsers?.map((user) => ( + {typeof users === 'undefined' && ( + + + + + + )} + {selectedUsers.map((user) => ( {user.fullName} {user?.affiliation} @@ -215,13 +238,61 @@ const DOIGenerationForm: React.FC = (props) => { - + Data + + + + + + + + + {/* TODO: do we need to display more info in this table? + we could rejig the fetch for users to return more info we want + as we're already querying every item in the cart there */} +
+ + + Name + + + + {cart + ?.filter( + (cartItem) => cartItem.entityType === currentTab + ) + .map((cartItem) => ( + + {cartItem.name} + + ))} + +
+
From 16c811c70cf49c0a4ef96bee7a72e9211782ad67 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 5 Jun 2023 12:23:28 +0100 Subject: [PATCH 09/68] #1529 - Fix typo that prevented single item carts from being minted --- packages/datagateway-download/src/downloadApiHooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 69e6b716c..678e47481 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -795,7 +795,7 @@ export const useIsCartMintable = ( return useQuery( ['ismintable', cart], () => { - if (doiMinterUrl && cart && cart.length > 1) + if (doiMinterUrl && cart && cart.length > 0) return isCartMintable(cart, doiMinterUrl); else return Promise.resolve(false); }, From 7c5491470921dce760598566b8c4d5f80427b30f Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 5 Jun 2023 14:44:52 +0100 Subject: [PATCH 10/68] #1531 - hide cart tabs that are empty and select a non-empty one by default --- .../DOIGenerationForm.component.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 20143501d..9cc80579b 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -50,6 +50,17 @@ const DOIGenerationForm: React.FC = (props) => { if (users) setSelectedUsers(users); }, [users]); + React.useEffect(() => { + if (cart) { + if (cart?.some((cartItem) => cartItem.entityType === 'investigation')) + setCurrentTab('investigation'); + if (cart?.some((cartItem) => cartItem.entityType === 'dataset')) + setCurrentTab('dataset'); + if (cart?.some((cartItem) => cartItem.entityType === 'datafile')) + setCurrentTab('datafile'); + } + }, [cart]); + return ( {acceptedDataPolicy ? ( @@ -261,9 +272,15 @@ const DOIGenerationForm: React.FC = (props) => { onChange={handleTabChange} aria-label="cart tabs" > - - - + {cart?.some( + (cartItem) => cartItem.entityType === 'investigation' + ) && } + {cart?.some( + (cartItem) => cartItem.entityType === 'dataset' + ) && } + {cart?.some( + (cartItem) => cartItem.entityType === 'datafile' + ) && } {/* TODO: do we need to display more info in this table? From be614572c5e45004b5dd04c7e334a04de3dd5c34 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 6 Jun 2023 15:35:07 +0100 Subject: [PATCH 11/68] #1531 - add mint DOI functionality also rejig the form to overflow better --- .../DOIGenerationForm.component.tsx | 145 +++++++++++------- .../datagateway-download/src/downloadApi.ts | 49 ++++++ .../src/downloadApiHooks.ts | 45 +++++- 3 files changed, 184 insertions(+), 55 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 9cc80579b..6feb1bc09 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -17,7 +17,12 @@ import { import { AxiosError } from 'axios'; import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; -import { useCart, useCartUsers, useCheckUser } from '../downloadApiHooks'; +import { + useCart, + useCartUsers, + useCheckUser, + useMintCart, +} from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; type DOIGenerationFormProps = { @@ -45,6 +50,7 @@ const DOIGenerationForm: React.FC = (props) => { const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); const { refetch: checkUser } = useCheckUser(email); + const { mutate: mintCart } = useMintCart(); React.useEffect(() => { if (users) setSelectedUsers(users); @@ -69,8 +75,73 @@ const DOIGenerationForm: React.FC = (props) => { Generate DOI - - + {/* use row-reverse, justifyContent start and the "wrong" order of components to make overflow layout nice + i.e. data summary presented at top before DOI form, but in non-overflow + mode it's DOI form on left and data summary on right */} + + + + + Data + + + + + + {cart?.some( + (cartItem) => cartItem.entityType === 'investigation' + ) && } + {cart?.some( + (cartItem) => cartItem.entityType === 'dataset' + ) && } + {cart?.some( + (cartItem) => cartItem.entityType === 'datafile' + ) && } + + + {/* TODO: do we need to display more info in this table? + we could rejig the fetch for users to return more info we want + as we're already querying every item in the cart there */} + + + + Name + + + + {cart + ?.filter( + (cartItem) => cartItem.entityType === currentTab + ) + .map((cartItem) => ( + + {cartItem.name} + + ))} + +
+
+
+ Details @@ -224,6 +295,7 @@ const DOIGenerationForm: React.FC = (props) => { {user?.email} - - - Data - - - - - {cart?.some( - (cartItem) => cartItem.entityType === 'investigation' - ) && } - {cart?.some( - (cartItem) => cartItem.entityType === 'dataset' - ) && } - {cart?.some( - (cartItem) => cartItem.entityType === 'datafile' - ) && } - - - {/* TODO: do we need to display more info in this table? - we could rejig the fetch for users to return more info we want - as we're already querying every item in the cart there */} - - - - Name - - - - {cart - ?.filter( - (cartItem) => cartItem.entityType === currentTab - ) - .map((cartItem) => ( - - {cartItem.name} - - ))} - -
-
-
diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 140227e14..221b16709 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -433,6 +433,55 @@ export const isCartMintable = async ( return status === 200; }; +export interface DoiMetadata { + title: string; + description: string; + creators: { email: string }[]; +} + +export interface DoiResult { + data_publication: string; + doi: string; +} + +/** + * Mint a DOI for a cart, returns a DataPublication ID & DOI + */ +export const mintCart = ( + cart: DownloadCartItem[], + doiMetadata: DoiMetadata, + settings: Pick +): Promise => { + const investigations: number[] = []; + const datasets: number[] = []; + const datafiles: number[] = []; + cart.forEach((cartItem) => { + if (cartItem.entityType === 'investigation') + investigations.push(cartItem.entityId); + if (cartItem.entityType === 'dataset') datasets.push(cartItem.entityId); + if (cartItem.entityType === 'datafile') datafiles.push(cartItem.entityId); + }); + return axios + .post( + `${settings.doiMinterUrl}/mint`, + { + metadata: { + ...doiMetadata, + resource_type: investigations.length === 0 ? 'Dataset' : 'Collection', + }, + investigations: { ids: investigations }, + datasets: { ids: datasets }, + datafiles: { ids: datafiles }, + }, + { + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, + }, + } + ) + .then((response) => response.data); +}; + const fetchEntityUsers = ( apiUrl: string, entityId: number, diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 678e47481..2cc71f7d9 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -33,10 +33,13 @@ import { import { DownloadSettingsContext } from './ConfigProvider'; import { checkUser, + DoiMetadata, + DoiResult, DownloadProgress, DownloadTypeStatus, getCartUsers, isCartMintable, + mintCart, SubmitCartZipType, } from './downloadApi'; import { @@ -787,7 +790,7 @@ export const useDownloadPercentageComplete = ({ * @param cart The {@link Cart} that is checked */ export const useIsCartMintable = ( - cart?: DownloadCartItem[] + cart: DownloadCartItem[] | undefined ): UseQueryResult => { const settings = React.useContext(DownloadSettingsContext); const { doiMinterUrl } = settings; @@ -825,6 +828,46 @@ export const useIsCartMintable = ( ); }; +/** + * Mints a cart + * @param cart The {@link Cart} to mint + * @param doiMetadata The required metadata for the DOI + */ +export const useMintCart = (): UseMutationResult< + DoiResult, + AxiosError, + { cart: DownloadCartItem[]; doiMetadata: DoiMetadata } +> => { + const settings = React.useContext(DownloadSettingsContext); + + return useMutation( + ({ cart, doiMetadata }) => { + return mintCart(cart, doiMetadata, settings); + }, + { + onError: (error) => { + log.error(error); + if (error.response?.status === 401) { + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: InvalidateTokenType, + payload: { + severity: 'error', + message: + localStorage.getItem('autoLogin') === 'true' + ? 'Your session has expired, please reload the page' + : 'Your session has expired, please login again', + }, + }, + }) + ); + } + }, + } + ); +}; + /** * Gets the total list of users associated with each item in the cart * @param cart The {@link Cart} that we're getting the users for From e0b8e7d74a55f6fa8088b78bdf5f4f34d9be8813 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 7 Jun 2023 10:18:48 +0100 Subject: [PATCH 12/68] #1531 - add DOI confirmation dialogue also rejig the overflow UI of the DOI generation form --- .../DOIConfirmDialog.component.tsx | 126 ++++ .../DOIGenerationForm.component.tsx | 558 +++++++++--------- .../src/downloadApiHooks.ts | 4 +- .../dialogTitle.component.tsx | 4 +- 4 files changed, 423 insertions(+), 269 deletions(-) create mode 100644 packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx new file mode 100644 index 000000000..320b44e4c --- /dev/null +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx @@ -0,0 +1,126 @@ +import { + Button, + CircularProgress, + Dialog, + Grid, + Typography, +} from '@mui/material'; +import { AxiosError } from 'axios'; +import { Mark } from 'datagateway-common'; +import React from 'react'; +import { QueryStatus } from 'react-query'; +import { Link } from 'react-router-dom'; + +import type { DoiResult } from '../downloadApi'; +import DialogContent from '../downloadConfirmation/dialogContent.component'; +import DialogTitle from '../downloadConfirmation/dialogTitle.component'; + +interface DOIConfirmDialogProps { + open: boolean; + mintingStatus: QueryStatus; + data: DoiResult | undefined; + error: AxiosError<{ + detail: { msg: string }[] | string; + }> | null; + + setClose: () => void; +} + +const DOIConfirmDialog: React.FC = ( + props: DOIConfirmDialogProps +) => { + const { open, mintingStatus, data, error, setClose } = props; + + const isMintError = mintingStatus === 'error'; + + const isMintSuccess = mintingStatus === 'success'; + + const isMintLoading = mintingStatus === 'loading'; + + return ( + { + if (isMintError) { + setClose(); + } + }} + open={open} + fullWidth={true} + maxWidth={'sm'} + > +
+ setClose() : undefined}> + Mint confirmation + + + + + {isMintSuccess ? ( + + ) : isMintError ? ( + + ) : isMintLoading ? ( + + ) : null} + + + {isMintSuccess ? ( + + Mint was successful + + ) : isMintError ? ( + + Mint was unsuccessful + + ) : ( + + Loading... + + )} + + {isMintSuccess && data && ( + + DOI: {data.doi} + + )} + + {isMintError && error && ( + + + Error:{' '} + {error.response?.data?.detail + ? typeof error.response.data.detail === 'string' + ? error.response.data.detail + : error.response.data.detail[0].msg + : error.message} + + + )} + + {isMintSuccess && data && ( + + + + )} + + +
+
+ ); +}; + +export default DOIConfirmDialog; diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 6feb1bc09..470fce135 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -24,6 +24,7 @@ import { useMintCart, } from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; +import DOIConfirmDialog from './DOIConfirmDialog.component'; type DOIGenerationFormProps = { lol?: string; @@ -39,6 +40,7 @@ const DOIGenerationForm: React.FC = (props) => { const [currentTab, setCurrentTab] = React.useState< 'investigation' | 'dataset' | 'datafile' >('investigation'); + const [showMintConfirmation, setShowMintConfirmation] = React.useState(false); const handleTabChange = ( event: React.SyntheticEvent, @@ -50,7 +52,12 @@ const DOIGenerationForm: React.FC = (props) => { const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); const { refetch: checkUser } = useCheckUser(email); - const { mutate: mintCart } = useMintCart(); + const { + mutateAsync: mintCart, + status: mintingStatus, + data: mintData, + error: mintError, + } = useMintCart(); React.useEffect(() => { if (users) setSelectedUsers(users); @@ -70,287 +77,306 @@ const DOIGenerationForm: React.FC = (props) => { return ( {acceptedDataPolicy ? ( - - - Generate DOI - - - {/* use row-reverse, justifyContent start and the "wrong" order of components to make overflow layout nice + <> + + + Generate DOI + + + {/* use row-reverse, justifyContent start and the "wrong" order of components to make overflow layout nice i.e. data summary presented at top before DOI form, but in non-overflow mode it's DOI form on left and data summary on right */} - - - - - Data - - - - - + + + + Data + + + + - {cart?.some( - (cartItem) => cartItem.entityType === 'investigation' - ) && } - {cart?.some( - (cartItem) => cartItem.entityType === 'dataset' - ) && } - {cart?.some( - (cartItem) => cartItem.entityType === 'datafile' - ) && } - - - {/* TODO: do we need to display more info in this table? + + {cart?.some( + (cartItem) => cartItem.entityType === 'investigation' + ) && ( + + )} + {cart?.some( + (cartItem) => cartItem.entityType === 'dataset' + ) && } + {cart?.some( + (cartItem) => cartItem.entityType === 'datafile' + ) && } + + + {/* TODO: do we need to display more info in this table? we could rejig the fetch for users to return more info we want as we're already querying every item in the cart there */} - - - - Name - - - - {cart - ?.filter( - (cartItem) => cartItem.entityType === currentTab - ) - .map((cartItem) => ( - - {cartItem.name} - - ))} - -
- - - - - - Details - - - - setTitle(event.target.value)} - /> - - - setDescription(event.target.value)} - /> + + + + Name + + + + {cart + ?.filter( + (cartItem) => cartItem.entityType === currentTab + ) + .map((cartItem) => ( + + {cartItem.name} + + ))} + +
+
- - - theme.palette.mode === 'dark' - ? theme.palette.grey[800] - : theme.palette.grey[100], - padding: 1, - }} - elevation={0} - variant="outlined" - > - - - - Creators - - - 0 ? 2 : 0 }} - > - - 0} - helperText={emailError.length > 0 ? emailError : ''} - sx={{ - backgroundColor: 'background.default', - // this CSS makes it so that the helperText doesn't mess with the button alignment - '& .MuiFormHelperText-root': { - position: 'absolute', - bottom: '-1.5rem', - }, - }} - value={email} - onChange={(event) => { - setEmail(event.target.value); - setEmailError(''); - }} - /> - + + + + Details + + + + setTitle(event.target.value)} + /> + + + setDescription(event.target.value)} + /> + + + + theme.palette.mode === 'dark' + ? theme.palette.grey[800] + : theme.palette.grey[100], + padding: 1, + }} + elevation={0} + variant="outlined" + > + - + + + + - Add Creator - - - - -
- - - Name - Affiliation - Email - Action - - - - {typeof users === 'undefined' && ( + - - - + Name + Affiliation + Email + Action - )} - {selectedUsers.map((user) => ( - - {user.fullName} - {user?.affiliation} - {user?.email} - - - - - ))} - -
+ + + + )} + {selectedUsers.map((user) => ( + + {user.fullName} + {user?.affiliation} + {user?.email} + + + + + ))} + + +
-
-
-
- - +
+ + + + - - -
+ +
+ {/* Show the download confirmation dialog. */} + setShowMintConfirmation(false)} + /> + ) : ( setAcceptedDataPolicy(true)} diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 2cc71f7d9..ab7a1f389 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -835,7 +835,9 @@ export const useIsCartMintable = ( */ export const useMintCart = (): UseMutationResult< DoiResult, - AxiosError, + AxiosError<{ + detail: { msg: string }[] | string; + }>, { cart: DownloadCartItem[]; doiMetadata: DoiMetadata } > => { const settings = React.useContext(DownloadSettingsContext); diff --git a/packages/datagateway-download/src/downloadConfirmation/dialogTitle.component.tsx b/packages/datagateway-download/src/downloadConfirmation/dialogTitle.component.tsx index 609f5aad6..dae91f59d 100644 --- a/packages/datagateway-download/src/downloadConfirmation/dialogTitle.component.tsx +++ b/packages/datagateway-download/src/downloadConfirmation/dialogTitle.component.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; interface DialogTitleProps { - id: string; - onClose: () => void; + id?: string; + onClose?: () => void; children?: React.ReactNode; } From 2bcd899ed107759e373d480dbdd15fe13de2bfae Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 7 Jun 2023 13:57:09 +0100 Subject: [PATCH 13/68] #1531 - change email to username in DOI creators stuff --- .../DOIGenerationForm.component.tsx | 49 +++++++---- .../datagateway-download/src/downloadApi.ts | 8 +- .../src/downloadApiHooks.ts | 84 ++++++++++--------- 3 files changed, 79 insertions(+), 62 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 470fce135..da1c8366b 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -33,8 +33,8 @@ type DOIGenerationFormProps = { const DOIGenerationForm: React.FC = (props) => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(true); const [selectedUsers, setSelectedUsers] = React.useState([]); - const [email, setEmail] = React.useState(''); - const [emailError, setEmailError] = React.useState(''); + const [username, setUsername] = React.useState(''); + const [usernameError, setUsernameError] = React.useState(''); const [title, setTitle] = React.useState(''); const [description, setDescription] = React.useState(''); const [currentTab, setCurrentTab] = React.useState< @@ -51,7 +51,7 @@ const DOIGenerationForm: React.FC = (props) => { const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); - const { refetch: checkUser } = useCheckUser(email); + const { refetch: checkUser } = useCheckUser(username); const { mutateAsync: mintCart, status: mintingStatus, @@ -200,16 +200,18 @@ const DOIGenerationForm: React.FC = (props) => { item spacing={1} alignItems="center" - sx={{ marginBottom: emailError.length > 0 ? 2 : 0 }} + sx={{ + marginBottom: usernameError.length > 0 ? 2 : 0, + }} > 0} + error={usernameError.length > 0} helperText={ - emailError.length > 0 ? emailError : '' + usernameError.length > 0 ? usernameError : '' } sx={{ backgroundColor: 'background.default', @@ -219,10 +221,10 @@ const DOIGenerationForm: React.FC = (props) => { bottom: '-1.5rem', }, }} - value={email} + value={username} onChange={(event) => { - setEmail(event.target.value); - setEmailError(''); + setUsername(event.target.value); + setUsernameError(''); }} /> @@ -233,7 +235,7 @@ const DOIGenerationForm: React.FC = (props) => { if ( selectedUsers.every( (selectedUser) => - selectedUser.email !== email + selectedUser.name !== username ) ) { checkUser({ throwOnError: true }) @@ -245,7 +247,7 @@ const DOIGenerationForm: React.FC = (props) => { ...selectedUsers, user, ]); - setEmail(''); + setUsername(''); } }) .catch( @@ -255,7 +257,7 @@ const DOIGenerationForm: React.FC = (props) => { }> ) => { // TODO: check this is the right message from the API - setEmailError( + setUsernameError( error.response?.data?.detail ? typeof error.response.data .detail === 'string' @@ -267,8 +269,8 @@ const DOIGenerationForm: React.FC = (props) => { } ); } else { - setEmailError('Cannot add duplicate user'); - setEmail(''); + setUsernameError('Cannot add duplicate user'); + setUsername(''); } }} > @@ -347,14 +349,25 @@ const DOIGenerationForm: React.FC = (props) => { onClick={() => { if (cart) { setShowMintConfirmation(true); + const creatorsList = selectedUsers + .filter( + (user) => + // the user requesting the mint is added automatically + // by the backend, so don't pass them to the backend + user.name !== readSciGatewayToken().username + ) + .map((user) => ({ + username: user.name, + })); mintCart({ cart, doiMetadata: { title, description, - creators: selectedUsers.map((user) => ({ - email: user.email ?? '', - })), + creators: + creatorsList.length > 0 + ? creatorsList + : undefined, }, }); } diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 221b16709..d4573dd05 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -436,7 +436,7 @@ export const isCartMintable = async ( export interface DoiMetadata { title: string; description: string; - creators: { email: string }[]; + creators?: { username: string }[]; } export interface DoiResult { @@ -571,15 +571,15 @@ export const getCartUsers = async ( }; /** - * Sends an email to the API and it checks if it's a valid ICAT User, on success + * Sends an username to the API and it checks if it's a valid ICAT User, on success * it returns the User, on failure it returns 404 */ export const checkUser = ( - email: string, + username: string, settings: Pick ): Promise => { return axios - .get(`${settings.doiMinterUrl}/user/${email}`, { + .get(`${settings.doiMinterUrl}/user/${username}`, { headers: { Authorization: `Bearer ${readSciGatewayToken().sessionId}`, }, diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index ab7a1f389..275d4d1bd 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -890,50 +890,54 @@ export const useCartUsers = ( }; /** - * Checks whether an email belongs to an ICAT User - * @param email The email that we're checking - * @returns the {@link User} that matches the email, or 404 + * Checks whether a username belongs to an ICAT User + * @param username The username that we're checking + * @returns the {@link User} that matches the username, or 404 */ export const useCheckUser = ( - email: string + username: string ): UseQueryResult => { const settings = React.useContext(DownloadSettingsContext); - return useQuery(['checkUser', email], () => checkUser(email, settings), { - onError: (error) => { - log.error(error); - if (error.response?.status === 401) { - document.dispatchEvent( - new CustomEvent(MicroFrontendId, { - detail: { - type: InvalidateTokenType, - payload: { - severity: 'error', - message: - localStorage.getItem('autoLogin') === 'true' - ? 'Your session has expired, please reload the page' - : 'Your session has expired, please login again', + return useQuery( + ['checkUser', username], + () => checkUser(username, settings), + { + onError: (error) => { + log.error(error); + if (error.response?.status === 401) { + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: InvalidateTokenType, + payload: { + severity: 'error', + message: + localStorage.getItem('autoLogin') === 'true' + ? 'Your session has expired, please reload the page' + : 'Your session has expired, please login again', + }, }, - }, - }) - ); - } - }, - retry: (failureCount: number, error: AxiosError) => { - if ( - // user not logged in, error code will log them out - error.response?.status === 401 || - // email doesn't match user - don't retry as this is a correct response from the server - error.response?.status === 404 || - // email is invalid - don't retry as this is correct response from the server - error.response?.status === 422 || - failureCount >= 3 - ) - return false; - return true; - }, - // set enabled false to only fetch on demand when the add creator button is pressed - enabled: false, - cacheTime: 0, - }); + }) + ); + } + }, + retry: (failureCount: number, error: AxiosError) => { + if ( + // user not logged in, error code will log them out + error.response?.status === 401 || + // email doesn't match user - don't retry as this is a correct response from the server + error.response?.status === 404 || + // email is invalid - don't retry as this is correct response from the server + error.response?.status === 422 || + failureCount >= 3 + ) + return false; + return true; + }, + // set enabled false to only fetch on demand when the add creator button is pressed + enabled: false, + cacheTime: 0, + } + ); }; From aaa194015c51b2849bf4d6b826b56cb753223a1a Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 7 Jun 2023 15:38:17 +0100 Subject: [PATCH 14/68] #1531 - prevent mint DOI page from being accessed directly also, turn accept data policy form back on and add a TODO and better placeholder text --- packages/datagateway-download/src/App.tsx | 1 - .../DOIGenerationForm.component.tsx | 16 ++++++++++------ .../acceptDataPolicy.component.tsx | 15 +++++++++------ .../downloadCart/downloadCartTable.component.tsx | 5 ++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/datagateway-download/src/App.tsx b/packages/datagateway-download/src/App.tsx index 74e9448f4..949eb9d34 100644 --- a/packages/datagateway-download/src/App.tsx +++ b/packages/datagateway-download/src/App.tsx @@ -96,7 +96,6 @@ class App extends Component { - {/* TODO: should users be able to access this link directly? or should we add a redirect back to download if accessed directly via URL */} diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index da1c8366b..b1470bab6 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -17,6 +17,7 @@ import { import { AxiosError } from 'axios'; import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; import { useCart, useCartUsers, @@ -26,12 +27,8 @@ import { import AcceptDataPolicy from './acceptDataPolicy.component'; import DOIConfirmDialog from './DOIConfirmDialog.component'; -type DOIGenerationFormProps = { - lol?: string; -}; - -const DOIGenerationForm: React.FC = (props) => { - const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(true); +const DOIGenerationForm: React.FC = () => { + const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(false); const [selectedUsers, setSelectedUsers] = React.useState([]); const [username, setUsername] = React.useState(''); const [usernameError, setUsernameError] = React.useState(''); @@ -74,6 +71,13 @@ const DOIGenerationForm: React.FC = (props) => { } }, [cart]); + const location = useLocation<{ fromCart: boolean } | undefined>(); + + // redirect if the user tries to access the link directly instead of from the cart + if (!location.state?.fromCart) { + return ; + } + return ( {acceptedDataPolicy ? ( diff --git a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx index ed4e599b9..b3339cd78 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx @@ -24,13 +24,16 @@ const AcceptDataPolicy: React.FC = (props) => { > + {/* TODO: write data policy text */} - Accept data policy wrghwurgh ergue rgeyrg erug er geurygerygueyrg - e rgeuygeurguerg rg ergyerg erg erg erg erg ergyeurgyuerg er - gerygueryg e rgeryguyerigy ergi er gery gergu i fg ewuefhuwef wef - wef weufw eufw ef wef wefiw efiwue f wefyweufy wfe wefy wfe - wiefwuiefi wue fw feiuwe fuwye uiewy fiwey fwy fewiufy iwyiefyiwue - f + Accept data policy: Lorem ipsum dolor sit amet, consectetur + adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate + velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint + occaecat cupidatat non proident, sunt in culpa qui officia + deserunt mollit anim id est laborum. diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index e053c3b2b..3ae3a915e 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -510,7 +510,10 @@ const DownloadCartTable: React.FC = ( color="primary" disabled={cartMintabilityLoading || !mintable} component={RouterLink} - to="/download/mint" + to={{ + pathname: '/download/mint', + state: { fromCart: true }, + }} > {t('downloadCart.generate_DOI')} From 43fd8b6f8a3c6865fdef2010610b733520cb915b Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 13 Jun 2023 13:56:25 +0100 Subject: [PATCH 15/68] #1531 - improve layout on larger screens --- .../src/DOIGenerationForm/DOIGenerationForm.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index b1470bab6..0b6763fa3 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -96,7 +96,7 @@ const DOIGenerationForm: React.FC = () => { justifyContent="start" spacing={2} > - + Data @@ -155,7 +155,7 @@ const DOIGenerationForm: React.FC = () => { - + Details From 8f26a07d167ced906ecb5b5eb833aa0073db6cf2 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 23 Jun 2023 14:24:42 +0100 Subject: [PATCH 16/68] Fix dark mode issues with buttons & tabs --- .../src/DOIGenerationForm/DOIGenerationForm.component.tsx | 4 ++++ .../src/DOIGenerationForm/acceptDataPolicy.component.tsx | 4 +++- .../src/downloadCart/downloadCartTable.component.tsx | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 0b6763fa3..47eb5fa27 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -113,6 +113,8 @@ const DOIGenerationForm: React.FC = () => { value={currentTab} onChange={handleTabChange} aria-label="cart tabs" + indicatorColor="secondary" + textColor="secondary" > {cart?.some( (cartItem) => cartItem.entityType === 'investigation' @@ -234,6 +236,7 @@ const DOIGenerationForm: React.FC = () => { diff --git a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx index b3339cd78..3fae14a40 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx @@ -37,7 +37,9 @@ const AcceptDataPolicy: React.FC = (props) => { - + diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index 3ae3a915e..98d070ce1 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -482,7 +482,7 @@ const DownloadCartTable: React.FC = ( className="tour-download-remove-button" id="removeAllButton" variant="outlined" - color="primary" + color="secondary" disabled={removingAll} startIcon={removingAll && } onClick={() => removeAllDownloadCartItems()} From 78d940495ac5a23b26d52ddf388184f2b7beeefa Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 23 Jun 2023 14:41:18 +0100 Subject: [PATCH 17/68] #1531 - fix a few more dark mode things --- .../DOIConfirmDialog.component.tsx | 2 +- .../DOIGenerationForm.component.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx index 320b44e4c..2f12facd0 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx @@ -67,7 +67,7 @@ const DOIConfirmDialog: React.FC = ( ) : isMintError ? ( ) : isMintLoading ? ( - + ) : null} diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 47eb5fa27..012abe732 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -79,11 +79,12 @@ const DOIGenerationForm: React.FC = () => { } return ( - + {acceptedDataPolicy ? ( <> - + {/* need to specify colour is textPrimary since this Typography is not in a Paper */} + Generate DOI @@ -168,6 +169,7 @@ const DOIGenerationForm: React.FC = () => { label="DOI Title" required fullWidth + color="secondary" value={title} onChange={(event) => setTitle(event.target.value)} /> @@ -179,6 +181,7 @@ const DOIGenerationForm: React.FC = () => { multiline rows={4} fullWidth + color="secondary" value={description} onChange={(event) => setDescription(event.target.value)} /> @@ -219,14 +222,19 @@ const DOIGenerationForm: React.FC = () => { helperText={ usernameError.length > 0 ? usernameError : '' } + color="secondary" sx={{ - backgroundColor: 'background.default', // this CSS makes it so that the helperText doesn't mess with the button alignment '& .MuiFormHelperText-root': { position: 'absolute', bottom: '-1.5rem', }, }} + InputProps={{ + sx: { + backgroundColor: 'background.default', + }, + }} value={username} onChange={(event) => { setUsername(event.target.value); From 7bdc362be2a5792becfa07277c92d612eb4b169c Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 23 Jun 2023 15:53:09 +0100 Subject: [PATCH 18/68] #1529 #1531 - don't pass empty arrays to minting API an update to the API meant empty arrays error --- packages/datagateway-download/src/downloadApi.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index d4573dd05..1c8273677 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -413,9 +413,11 @@ export const isCartMintable = async ( .post( `${doiMinterUrl}/ismintable`, { - investigations: { ids: investigations }, - datasets: { ids: datasets }, - datafiles: { ids: datafiles }, + ...(investigations.length > 0 + ? { investigations: { ids: investigations } } + : {}), + ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), + ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), }, { headers: { @@ -469,9 +471,11 @@ export const mintCart = ( ...doiMetadata, resource_type: investigations.length === 0 ? 'Dataset' : 'Collection', }, - investigations: { ids: investigations }, - datasets: { ids: datasets }, - datafiles: { ids: datafiles }, + ...(investigations.length > 0 + ? { investigations: { ids: investigations } } + : {}), + ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), + ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), }, { headers: { From 47cb96989a4415622c533e2f38e15884ed31dc1b Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 27 Jun 2023 12:05:20 +0100 Subject: [PATCH 19/68] #1529 - highlight which rows a user doesn't have permission to mint also fix some errors in testing error codes --- packages/datagateway-common/src/api/cart.tsx | 4 +- .../DOIGenerationForm.component.tsx | 2 +- .../datagateway-download/src/downloadApi.ts | 35 +++++-------- .../src/downloadApiHooks.ts | 21 +++++--- .../downloadCartTable.component.tsx | 51 +++++++++++++++++-- 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/packages/datagateway-common/src/api/cart.tsx b/packages/datagateway-common/src/api/cart.tsx index 2a01942f1..9cd4b5b3f 100644 --- a/packages/datagateway-common/src/api/cart.tsx +++ b/packages/datagateway-common/src/api/cart.tsx @@ -100,7 +100,7 @@ export const useAddToCart = ( }, retry: (failureCount, error) => { // if we get 431 we know this is an intermittent error so retry - if (error.code === '431' && failureCount < 3) { + if (error.response?.status === 431 && failureCount < 3) { return true; } else { return false; @@ -141,7 +141,7 @@ export const useRemoveFromCart = ( }, retry: (failureCount, error) => { // if we get 431 we know this is an intermittent error so retry - if (error.code === '431' && failureCount < 3) { + if (error.response?.status === 431 && failureCount < 3) { return true; } else { return false; diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 012abe732..a34cfb2d3 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -50,7 +50,7 @@ const DOIGenerationForm: React.FC = () => { const { data: users } = useCartUsers(cart); const { refetch: checkUser } = useCheckUser(username); const { - mutateAsync: mintCart, + mutate: mintCart, status: mintingStatus, data: mintData, error: mintError, diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 1c8273677..cd3938847 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -409,28 +409,21 @@ export const isCartMintable = async ( if (cartItem.entityType === 'dataset') datasets.push(cartItem.entityId); if (cartItem.entityType === 'datafile') datafiles.push(cartItem.entityId); }); - const { status } = await axios - .post( - `${doiMinterUrl}/ismintable`, - { - ...(investigations.length > 0 - ? { investigations: { ids: investigations } } - : {}), - ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), - ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), + const { status } = await axios.post( + `${doiMinterUrl}/ismintable`, + { + ...(investigations.length > 0 + ? { investigations: { ids: investigations } } + : {}), + ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), + ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), + }, + { + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, }, - { - headers: { - Authorization: `Bearer ${readSciGatewayToken().sessionId}`, - }, - } - ) - .catch((error) => { - // catch 403 error as it's not really an error! - if (axios.isAxiosError(error) && error.response?.status === 403) - return error.response; - throw error; - }); + } + ); return status === 200; }; diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 275d4d1bd..fd7c45ae6 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -137,7 +137,7 @@ export const useRemoveAllFromCart = (): UseMutationResult< }, retry: (failureCount, error) => { // if we get 431 we know this is an intermittent error so retry - if (error.code === '431' && failureCount < 3) { + if (error.response?.status === 431 && failureCount < 3) { return true; } else { return false; @@ -171,7 +171,7 @@ export const useRemoveEntityFromCart = (): UseMutationResult< }, retry: (failureCount, error) => { // if we get 431 we know this is an intermittent error so retry - if (error.code === '431' && failureCount < 3) { + if (error.response?.status === 431 && failureCount < 3) { return true; } else { return false; @@ -485,7 +485,7 @@ export const useDownloadOrRestoreDownload = (): UseMutationResult< retry: (failureCount, error) => { // if we get 431 we know this is an intermittent error so retry - return error.code === '431' && failureCount < 3; + return error.response?.status === 431 && failureCount < 3; }, } ); @@ -791,7 +791,7 @@ export const useDownloadPercentageComplete = ({ */ export const useIsCartMintable = ( cart: DownloadCartItem[] | undefined -): UseQueryResult => { +): UseQueryResult> => { const settings = React.useContext(DownloadSettingsContext); const { doiMinterUrl } = settings; @@ -804,7 +804,7 @@ export const useIsCartMintable = ( }, { onError: (error) => { - log.error(error); + if (error.response?.status !== 403) log.error(error); if (error.response?.status === 401) { document.dispatchEvent( new CustomEvent(MicroFrontendId, { @@ -822,7 +822,16 @@ export const useIsCartMintable = ( ); } }, - staleTime: Infinity, + retry: (failureCount, error) => { + // if we get 403 we know this is an legit response from the backend so don't bother retrying + // all other errors use default retry behaviour + if (error.response?.status === 403 || failureCount >= 3) { + return false; + } else { + return true; + } + }, + refetchOnWindowFocus: false, enabled: typeof doiMinterUrl !== 'undefined', } ); diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index 98d070ce1..524902dda 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -63,8 +63,11 @@ const DownloadCartTable: React.FC = ( const { mutate: removeAllDownloadCartItems, isLoading: removingAll } = useRemoveAllFromCart(); const { data, isFetching: dataLoading } = useCart(); - const { data: mintable, isLoading: cartMintabilityLoading } = - useIsCartMintable(data); + const { + data: mintable, + isLoading: cartMintabilityLoading, + error: mintableError, + } = useIsCartMintable(data); const fileCountQueries = useDatafileCounts(data); const sizeQueries = useSizes(data); @@ -167,6 +170,31 @@ const DownloadCartTable: React.FC = ( return filteredData?.sort(sortCartItems); }, [data, sort, filters, sizeQueries, fileCountQueries]); + const unmintableEntityIDs: number[] | null | undefined = React.useMemo( + () => + mintableError?.response?.data.detail && + JSON.parse( + mintableError.response.data.detail.substring( + mintableError.response.data.detail.indexOf('['), + mintableError.response.data.detail.lastIndexOf(']') + 1 + ) + ), + [mintableError] + ); + + const unmintableRowIDs = React.useMemo(() => { + if (unmintableEntityIDs && sortedAndFilteredData) { + return unmintableEntityIDs.map((id) => + sortedAndFilteredData.findIndex((entity) => entity.entityId === id) + ); + } else { + return []; + } + }, [unmintableEntityIDs, sortedAndFilteredData]); + + const [generateDOIButtonHover, setGenerateDOIButtonHover] = + React.useState(false); + const columns: ColumnType[] = React.useMemo( () => [ { @@ -329,6 +357,20 @@ const DownloadCartTable: React.FC = ( }${dataLoading ? ' - 4px' : ''} - (1.75 * 0.875rem + 12px))`, minHeight: 230, overflowX: 'auto', + // handle the highlight of unmintable entities + ...(generateDOIButtonHover && { + '& [role="rowgroup"] [role="row"]': Object.assign( + {}, + ...unmintableRowIDs.map((id) => ({ + [`&:nth-of-type(${id + 1})`]: { + bgcolor: 'error.main', + '& [role="gridcell"] *': { + color: 'error.contrastText', + }, + }, + })) + ), + }), }} > = ( } > {/* need this span so the tooltip works when the button is disabled */} - + setGenerateDOIButtonHover(true)} + onMouseLeave={() => setGenerateDOIButtonHover(false)} + > )} diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index a34cfb2d3..fd473c3e9 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -17,6 +17,7 @@ import { import { AxiosError } from 'axios'; import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Redirect, useLocation } from 'react-router-dom'; import { useCart, @@ -73,6 +74,8 @@ const DOIGenerationForm: React.FC = () => { const location = useLocation<{ fromCart: boolean } | undefined>(); + const [t] = useTranslation(); + // redirect if the user tries to access the link directly instead of from the cart if (!location.state?.fromCart) { return ; @@ -85,7 +88,7 @@ const DOIGenerationForm: React.FC = () => { {/* need to specify colour is textPrimary since this Typography is not in a Paper */} - Generate DOI + {t('DOIGenerationForm.page_header')} {/* use row-reverse, justifyContent start and the "wrong" order of components to make overflow layout nice @@ -100,7 +103,7 @@ const DOIGenerationForm: React.FC = () => { - Data + {t('DOIGenerationForm.data_header')} @@ -113,21 +116,36 @@ const DOIGenerationForm: React.FC = () => { {cart?.some( (cartItem) => cartItem.entityType === 'investigation' ) && ( - + )} {cart?.some( (cartItem) => cartItem.entityType === 'dataset' - ) && } + ) && ( + + )} {cart?.some( (cartItem) => cartItem.entityType === 'datafile' - ) && } + ) && ( + + )} {/* TODO: do we need to display more info in this table? @@ -141,7 +159,9 @@ const DOIGenerationForm: React.FC = () => { > - Name + + {t('DOIGenerationForm.cart_table_name')} + @@ -161,12 +181,12 @@ const DOIGenerationForm: React.FC = () => { - Details + {t('DOIGenerationForm.form_header')} { { - Creators + {t('DOIGenerationForm.creators')} { > 0} @@ -289,7 +309,7 @@ const DOIGenerationForm: React.FC = () => { } }} > - Add Creator + {t('DOIGenerationForm.add_creator')} @@ -302,10 +322,18 @@ const DOIGenerationForm: React.FC = () => { > - Name - Affiliation - Email - Action + + {t('DOIGenerationForm.creator_name')} + + + {t('DOIGenerationForm.creator_affiliation')} + + + {t('DOIGenerationForm.creator_email')} + + + {t('DOIGenerationForm.creator_action')} + @@ -341,7 +369,7 @@ const DOIGenerationForm: React.FC = () => { } color="secondary" > - Delete + {t('DOIGenerationForm.delete_creator')} @@ -389,7 +417,7 @@ const DOIGenerationForm: React.FC = () => { } }} > - Generate DOI + {t('DOIGenerationForm.generate_DOI')} diff --git a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx index 3fae14a40..57c17f91f 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/acceptDataPolicy.component.tsx @@ -1,11 +1,13 @@ import { Button, Grid, Paper, Typography } from '@mui/material'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type AcceptDataPolicyProps = { acceptDataPolicy: () => void; }; const AcceptDataPolicy: React.FC = (props) => { + const [t] = useTranslation(); return ( = (props) => { {/* TODO: write data policy text */} - Accept data policy: Lorem ipsum dolor sit amet, consectetur - adipiscing elit, sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate - velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint - occaecat cupidatat non proident, sunt in culpa qui officia - deserunt mollit anim id est laborum. + {t('acceptDataPolicy.data_policy')} From f339c6191ae1dc816bd77ba2bfb35f477b887e0a Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 4 Jul 2023 10:07:23 +0100 Subject: [PATCH 23/68] #1529 #1531 - fix broken unit tests --- packages/datagateway-common/src/api/cart.test.tsx | 14 +++++++++----- .../src/downloadApiHooks.test.tsx | 14 +++++++++----- ...dminDownloadStatusTable.component.test.tsx.snap | 12 ++++++------ .../downloadStatusTable.component.test.tsx.snap | 2 +- .../downloadTab.component.test.tsx.snap | 4 ++-- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/datagateway-common/src/api/cart.test.tsx b/packages/datagateway-common/src/api/cart.test.tsx index 2178a1591..c4c9bce9d 100644 --- a/packages/datagateway-common/src/api/cart.test.tsx +++ b/packages/datagateway-common/src/api/cart.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { useCart, useAddToCart, useRemoveFromCart } from '.'; import { DownloadCart } from '../app.types'; import handleICATError from '../handleICATError'; @@ -122,9 +122,11 @@ describe('Cart api functions', () => { it('sends axios request to add item to cart once mutate function is called and calls handleICATError on failure, with a retry on code 431', async () => { (axios.post as jest.MockedFunction) .mockRejectedValueOnce({ - code: '431', + response: { + status: 431, + }, message: 'Test 431 error message', - }) + } as AxiosError) .mockRejectedValue({ message: 'Test error message', }); @@ -184,9 +186,11 @@ describe('Cart api functions', () => { it('sends axios request to remove item from cart once mutate function is called and calls handleICATError on failure, with a retry on code 431', async () => { (axios.post as jest.MockedFunction) .mockRejectedValueOnce({ - code: '431', + response: { + status: 431, + }, message: 'Test 431 error message', - }) + } as AxiosError) .mockRejectedValue({ message: 'Test error message', }); diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx index 4e828cd9b..713d74c57 100644 --- a/packages/datagateway-download/src/downloadApiHooks.test.tsx +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -1,5 +1,5 @@ import { renderHook, WrapperComponent } from '@testing-library/react-hooks'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import type { Download } from 'datagateway-common'; import { DownloadCartItem, @@ -177,9 +177,11 @@ describe('Download Cart API react-query hooks test', () => { .fn() .mockImplementationOnce(() => Promise.reject({ - code: '431', + response: { + status: 431, + }, message: 'Test 431 error message', - }) + } as AxiosError) ) .mockImplementation(() => Promise.reject({ @@ -243,9 +245,11 @@ describe('Download Cart API react-query hooks test', () => { .fn() .mockImplementationOnce(() => Promise.reject({ - code: '431', + response: { + status: 431, + }, message: 'Test 431 error message', - }) + } as AxiosError) ) .mockImplementation(() => Promise.reject({ diff --git a/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap b/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap index a03f73441..322550895 100644 --- a/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap +++ b/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap @@ -72,7 +72,7 @@ exports[`Admin Download Status Table should render correctly 1`] = `
@@ -1343,7 +1343,7 @@ exports[`Admin Download Status Table should render correctly 1`] = `
@@ -1628,7 +1628,7 @@ exports[`Admin Download Status Table should render correctly 1`] = `
@@ -1913,7 +1913,7 @@ exports[`Admin Download Status Table should render correctly 1`] = `
@@ -2177,7 +2177,7 @@ exports[`Admin Download Status Table should render correctly 1`] = `
diff --git a/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap b/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap index b1fbca3b6..48bcdcb65 100644 --- a/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap +++ b/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap @@ -32,7 +32,7 @@ exports[`Download Status Table should render correctly 1`] = `
- -
- -
-
-
-
-
-
-
- - - - -
-
-
-
-
-
-
-
-
-
-
-
- -

- downloadCart.name -

- -
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -

- downloadCart.type -

- -
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -

- downloadCart.size -

- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -

- downloadCart.fileCount -

- -
-
-
-
-
-
-
-
-
-
-
- Actions -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - downloadCart.number_of_files: 0 / 5000 - -
-
-
-
-
- - downloadCart.total_size: 0 B / 1 TB - -
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- - -`; diff --git a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx index 3948c60ff..569911e1e 100644 --- a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx +++ b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx @@ -1,7 +1,6 @@ -import type { RenderResult } from '@testing-library/react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { RenderResult } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { UserEvent } from '@testing-library/user-event/dist/types/setup'; import { fetchDownloadCart } from 'datagateway-common'; import { createMemoryHistory } from 'history'; import * as React from 'react'; @@ -16,6 +15,7 @@ import { getSize, removeAllDownloadCartItems, removeFromCart, + isCartMintable, } from '../downloadApi'; import { mockCartItems, mockDownloadItems, mockedSettings } from '../testData'; import DownloadTabs from './downloadTab.component'; @@ -33,7 +33,7 @@ jest.mock('../downloadApi'); describe('DownloadTab', () => { let history; let holder; - let user: UserEvent; + let user: ReturnType; beforeEach(() => { history = createMemoryHistory(); @@ -55,7 +55,7 @@ describe('DownloadTab', () => { removeAllDownloadCartItems as jest.MockedFunction< typeof removeAllDownloadCartItems > - ).mockResolvedValue(null); + ).mockResolvedValue(); ( removeFromCart as jest.MockedFunction ).mockImplementation((entityType, entityIds) => { @@ -68,6 +68,9 @@ describe('DownloadTab', () => { ( getDatafileCount as jest.MockedFunction ).mockResolvedValue(7); + ( + isCartMintable as jest.MockedFunction + ).mockResolvedValue(true); }); const renderComponent = (): RenderResult => { @@ -83,11 +86,6 @@ describe('DownloadTab', () => { ); }; - it('should render correctly', () => { - const { asFragment } = renderComponent(); - expect(asFragment()).toMatchSnapshot(); - }); - it('shows the appropriate table when clicking between tabs', async () => { renderComponent(); @@ -95,34 +93,54 @@ describe('DownloadTab', () => { await user.click(await screen.findByText('downloadTab.downloads_tab')); - await waitFor(async () => { - expect( - await screen.findByLabelText( - 'downloadTab.download_cart_panel_arialabel' - ) - ).not.toBeVisible(); - expect( - await screen.findByLabelText( - 'downloadTab.download_status_panel_arialabel' - ) - ).toBeVisible(); - }); + expect( + await screen.findByLabelText('downloadTab.download_cart_panel_arialabel') + ).not.toBeVisible(); + expect( + await screen.findByLabelText( + 'downloadTab.download_status_panel_arialabel' + ) + ).toBeVisible(); // Return back to the cart tab. await user.click(await screen.findByText('downloadTab.cart_tab')); - await waitFor(async () => { - expect( - await screen.findByLabelText( - 'downloadTab.download_cart_panel_arialabel' - ) - ).toBeVisible(); - expect( - await screen.findByLabelText( - 'downloadTab.download_status_panel_arialabel' - ) - ).not.toBeVisible(); - }); + expect( + await screen.findByLabelText('downloadTab.download_cart_panel_arialabel') + ).toBeVisible(); + expect( + await screen.findByLabelText( + 'downloadTab.download_status_panel_arialabel' + ) + ).not.toBeVisible(); + }); + + it('refreshes downloads when the refresh button is clicked', async () => { + renderComponent(); + + ( + fetchDownloads as jest.MockedFunction + ).mockImplementation( + () => + new Promise((_) => { + // do nothing, simulating pending promise + // to test refreshing state + }) + ); + + // go to downloads tab + + await user.click(await screen.findByText('downloadTab.downloads_tab')); + + await user.click( + screen.getByRole('button', { + name: 'downloadTab.refresh_download_status_arialabel', + }) + ); + + expect( + await screen.findByText('downloadTab.refreshing_downloads') + ).toBeInTheDocument(); }); }); From 701b7423672a60321a7260d1be849f4d564e766a Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Fri, 7 Jul 2023 14:23:18 +0100 Subject: [PATCH 26/68] #1531 - add tests for DOIGenerationForm --- .../DOIGenerationForm.component.test.tsx | 453 ++++++++++++++++++ .../DOIGenerationForm.component.tsx | 13 +- 2 files changed, 462 insertions(+), 4 deletions(-) create mode 100644 packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx new file mode 100644 index 000000000..47aaa528f --- /dev/null +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -0,0 +1,453 @@ +import { + render, + RenderResult, + screen, + waitForElementToBeRemoved, + within, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { fetchDownloadCart } from 'datagateway-common'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import * as React from 'react'; +import { QueryClient, QueryClientProvider, setLogger } from 'react-query'; +import { Router } from 'react-router-dom'; +import { DownloadSettingsContext } from '../ConfigProvider'; +import { mockCartItems, mockedSettings } from '../testData'; +import { + checkUser, + getCartUsers, + isCartMintable, + mintCart, +} from '../downloadApi'; +import DOIGenerationForm from './DOIGenerationForm.component'; + +setLogger({ + log: console.log, + warn: console.warn, + error: jest.fn(), +}); + +jest.mock('datagateway-common', () => { + const originalModule = jest.requireActual('datagateway-common'); + + return { + __esModule: true, + ...originalModule, + fetchDownloadCart: jest.fn(), + readSciGatewayToken: jest.fn(() => ({ + username: 'user1', + })), + }; +}); + +jest.mock('../downloadApi', () => { + const originalModule = jest.requireActual('../downloadApi'); + + return { + ...originalModule, + isCartMintable: jest.fn(), + getCartUsers: jest.fn(), + checkUser: jest.fn(), + mintCart: jest.fn(), + }; +}); + +const createTestQueryClient = (): QueryClient => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + +const renderComponent = ( + history = createMemoryHistory({ + initialEntries: [{ pathname: '/download/mint', state: { fromCart: true } }], + }) +): RenderResult & { history: MemoryHistory } => ({ + history, + ...render( + + + + + + + + ), +}); + +describe('Download cart table component', () => { + let user: ReturnType; + + beforeEach(() => { + user = userEvent.setup(); + + ( + fetchDownloadCart as jest.MockedFunction + ).mockResolvedValue(mockCartItems); + + ( + isCartMintable as jest.MockedFunction + ).mockResolvedValue(true); + + // mock mint cart error to test dialog can be closed after it errors + (mintCart as jest.MockedFunction).mockRejectedValue( + 'error' + ); + + ( + getCartUsers as jest.MockedFunction + ).mockResolvedValue([ + { + id: 1, + name: 'user1', + fullName: 'User 1', + email: 'user1@example.com', + affiliation: 'Example Uni', + }, + { + id: 2, + name: 'user2', + fullName: 'User 2', + email: 'user2@example.com', + affiliation: 'Example 2 Uni', + }, + ]); + + (checkUser as jest.MockedFunction).mockResolvedValue({ + id: 3, + name: 'user3', + fullName: 'User 3', + email: 'user3@example.com', + affiliation: 'Example 3 Uni', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should redirect back to /download if user directly accesses the url', async () => { + const { history } = renderComponent(createMemoryHistory()); + + expect(history.location).toMatchObject({ pathname: '/download' }); + }); + + it('should render the data policy before loading the form', async () => { + renderComponent(); + + expect( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ).toBeInTheDocument(); + expect( + screen.queryByText('DOIGenerationForm.page_header') + ).not.toBeInTheDocument(); + }); + + it('should let the user fill in the required fields and submit a mint request', async () => { + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), + 'title' + ); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), + 'desc' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ); + + expect( + await screen.findByRole('dialog', { + name: 'DOIConfirmDialog.dialog_title', + }) + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { + name: 'downloadConfirmDialog.close_arialabel', + }) + ); + + await waitForElementToBeRemoved(() => + screen.queryByRole('dialog', { + name: 'DOIConfirmDialog.dialog_title', + }) + ); + }); + + it('should let the user delete users (but not delete the logged in user)', async () => { + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(2); + expect( + screen.getByRole('cell', { name: 'user2@example.com' }) + ).toBeInTheDocument(); + + const userDeleteButtons = screen.getAllByRole('button', { + name: 'DOIGenerationForm.delete_creator', + }); + expect(userDeleteButtons[0]).toBeDisabled(); + + await user.click(userDeleteButtons[1]); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) + ).toHaveLength(1); + expect( + screen.getByRole('cell', { name: 'Example Uni' }) + ).toBeInTheDocument(); + }); + + it('should let the user add users (but not duplicate users or if checkUser fails)', async () => { + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(2); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), + 'user3' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + expect(screen.getByRole('cell', { name: 'User 3' })).toBeInTheDocument(); + + // test errors on duplicate user + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), + 'user3' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + expect(screen.getByText('Cannot add duplicate user')).toBeInTheDocument(); + expect( + screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }) + ).toHaveValue(''); + + // test errors with various API error responses + (checkUser as jest.MockedFunction).mockRejectedValueOnce({ + response: { data: { detail: 'error msg' }, status: 404 }, + }); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), + 'user4' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ); + + expect(await screen.findByText('error msg')).toBeInTheDocument(); + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + + (checkUser as jest.MockedFunction).mockRejectedValue({ + response: { data: { detail: [{ msg: 'error msg 2' }] }, status: 404 }, + }); + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ); + + expect(await screen.findByText('error msg 2')).toBeInTheDocument(); + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + + (checkUser as jest.MockedFunction).mockRejectedValueOnce({ + response: { status: 422 }, + }); + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ); + + expect(await screen.findByText('Error')).toBeInTheDocument(); + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + }); + + it('should let the user change cart tabs', async () => { + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'cart investigation table' }) + ).getByRole('cell', { name: 'INVESTIGATION 1' }) + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('tab', { name: 'DOIGenerationForm.cart_tab_datasets' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'cart dataset table' }) + ).getByRole('cell', { name: 'DATASET 1' }) + ).toBeInTheDocument(); + }); + + describe('only displays cart tabs if the corresponding entity type exists in the cart: ', () => { + it('investigations', async () => { + ( + fetchDownloadCart as jest.MockedFunction + ).mockResolvedValue([mockCartItems[0]]); + + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'cart investigation table' }) + ).getByRole('cell', { name: 'INVESTIGATION 1' }) + ).toBeInTheDocument(); + + expect( + screen.getByRole('tab', { + name: 'DOIGenerationForm.cart_tab_investigations', + }) + ).toBeInTheDocument(); + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_datasets', + }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_datafiles', + }) + ).not.toBeInTheDocument(); + }); + + it('datasets', async () => { + ( + fetchDownloadCart as jest.MockedFunction + ).mockResolvedValue([mockCartItems[2]]); + + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'cart dataset table' }) + ).getByRole('cell', { name: 'DATASET 1' }) + ).toBeInTheDocument(); + + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_investigations', + }) + ).not.toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: 'DOIGenerationForm.cart_tab_datasets' }) + ).toBeInTheDocument(); + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_datafiles', + }) + ).not.toBeInTheDocument(); + }); + + it('datafiles', async () => { + ( + fetchDownloadCart as jest.MockedFunction + ).mockResolvedValue([mockCartItems[3]]); + + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'cart datafile table' }) + ).getByRole('cell', { name: 'DATAFILE 1' }) + ).toBeInTheDocument(); + + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_investigations', + }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('tab', { + name: 'DOIGenerationForm.cart_tab_datasets', + }) + ).not.toBeInTheDocument(); + expect( + screen.getByRole('tab', { + name: 'DOIGenerationForm.cart_tab_datafiles', + }) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index fd473c3e9..bdd9c1b41 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -65,9 +65,9 @@ const DOIGenerationForm: React.FC = () => { if (cart) { if (cart?.some((cartItem) => cartItem.entityType === 'investigation')) setCurrentTab('investigation'); - if (cart?.some((cartItem) => cartItem.entityType === 'dataset')) + else if (cart?.some((cartItem) => cartItem.entityType === 'dataset')) setCurrentTab('dataset'); - if (cart?.some((cartItem) => cartItem.entityType === 'datafile')) + else if (cart?.some((cartItem) => cartItem.entityType === 'datafile')) setCurrentTab('datafile'); } }, [cart]); @@ -156,6 +156,7 @@ const DOIGenerationForm: React.FC = () => { backgroundColor: 'background.default', }} size="small" + aria-label={`cart ${currentTab} table`} > @@ -220,7 +221,11 @@ const DOIGenerationForm: React.FC = () => { > - + {t('DOIGenerationForm.creators')} @@ -319,6 +324,7 @@ const DOIGenerationForm: React.FC = () => { backgroundColor: 'background.default', }} size="small" + aria-labelledby="creators-label" > @@ -426,7 +432,6 @@ const DOIGenerationForm: React.FC = () => { {/* Show the download confirmation dialog. */} Date: Tue, 18 Jul 2023 12:07:18 +0100 Subject: [PATCH 27/68] #1531 - add DOIConfirmDialog tests --- .../public/res/default.json | 4 +- .../DOIConfirmDialog.component.test.tsx | 89 +++++++++++++++++++ .../DOIConfirmDialog.component.tsx | 16 ++-- 3 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx diff --git a/packages/datagateway-download/public/res/default.json b/packages/datagateway-download/public/res/default.json index 37712ea20..130d25b77 100644 --- a/packages/datagateway-download/public/res/default.json +++ b/packages/datagateway-download/public/res/default.json @@ -127,8 +127,8 @@ "mint_success": "Mint was successful", "mint_error": "Mint was unsuccessful", "mint_loading": "Loading...", - "doi_label": "DOI: ", - "error_label": "Error: ", + "doi_label": "DOI", + "error_label": "Error", "view_data_publication": "View Data Publication" } } diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx new file mode 100644 index 000000000..477d0467e --- /dev/null +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx @@ -0,0 +1,89 @@ +import { render, RenderResult, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import * as React from 'react'; +import { Router } from 'react-router-dom'; +import DOIConfirmDialog from './DOIConfirmDialog.component'; + +describe('Download cart table component', () => { + let user: ReturnType; + let props: React.ComponentProps; + + const renderComponent = (): RenderResult & { history: MemoryHistory } => { + const history = createMemoryHistory(); + return { + history, + ...render( + + + + ), + }; + }; + + beforeEach(() => { + user = userEvent.setup(); + props = { + open: true, + mintingStatus: 'loading', + data: undefined, + error: null, + setClose: jest.fn(), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should show loading indicator when mintingStatus is loading', async () => { + renderComponent(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect( + screen.getByText('DOIConfirmDialog.mint_loading') + ).toBeInTheDocument(); + + // expect user can't close the dialog + await user.type(screen.getByRole('dialog'), '{Esc}'); + expect( + screen.queryByRole('button', { + name: 'downloadConfirmDialog.close_arialabel', + }) + ).not.toBeInTheDocument(); + expect(props.setClose).not.toHaveBeenCalled(); + }); + + it('should show success indicators when mintingStatus is success and allow user to view their data publication', async () => { + props.mintingStatus = 'success'; + props.data = { data_publication: '123456', doi: 'test_doi' }; + const { history } = renderComponent(); + + expect( + screen.getByText('DOIConfirmDialog.mint_success') + ).toBeInTheDocument(); + expect(screen.getByText('test_doi', { exact: false })).toBeInTheDocument(); + + await user.click( + screen.getByRole('link', { + name: 'DOIConfirmDialog.view_data_publication', + }) + ); + expect(history.location).toMatchObject({ + pathname: `/browse/dataPublication/${props.data.data_publication}`, + }); + }); + + it('should show error indicators when mintingStatus is error and allow user to close the dialog', async () => { + props.mintingStatus = 'error'; + props.error = { response: { data: { detail: 'error msg' } } }; + renderComponent(); + + expect(screen.getByText('DOIConfirmDialog.mint_error')).toBeInTheDocument(); + expect(screen.getByText('error msg', { exact: false })).toBeInTheDocument(); + + // use Esc to close dialog + await user.type(screen.getByRole('dialog'), '{Esc}'); + expect(props.setClose).toHaveBeenCalled(); + }); +}); diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx index 510b42d48..6bd2356fc 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.tsx @@ -91,7 +91,7 @@ const DOIConfirmDialog: React.FC = ( {isMintSuccess && data && ( - {t('DOIConfirmDialog.doi_label') + data.doi} + {`${t('DOIConfirmDialog.doi_label')}: ${data.doi}`} )} @@ -99,12 +99,14 @@ const DOIConfirmDialog: React.FC = ( {isMintError && error && ( - {t('DOIConfirmDialog.error_label') + - (error.response?.data?.detail - ? typeof error.response.data.detail === 'string' - ? error.response.data.detail - : error.response.data.detail[0].msg - : error.message)} + {`${t('DOIConfirmDialog.error_label')}: + ${ + error.response?.data?.detail + ? typeof error.response.data.detail === 'string' + ? error.response.data.detail + : error.response.data.detail[0].msg + : error.message + }`} )} From ffe241b090419c9e8ef8eb2f3a4d16335976252f Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 18 Jul 2023 12:49:26 +0100 Subject: [PATCH 28/68] #1531 - fix names of tests & decrease characters typed --- .../DOIConfirmDialog.component.test.tsx | 2 +- .../DOIGenerationForm.component.test.tsx | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx index 477d0467e..96febc3e4 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIConfirmDialog.component.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Router } from 'react-router-dom'; import DOIConfirmDialog from './DOIConfirmDialog.component'; -describe('Download cart table component', () => { +describe('DOI Confirm Dialogue component', () => { let user: ReturnType; let props: React.ComponentProps; diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index 47aaa528f..74a39cb21 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -35,7 +35,7 @@ jest.mock('datagateway-common', () => { ...originalModule, fetchDownloadCart: jest.fn(), readSciGatewayToken: jest.fn(() => ({ - username: 'user1', + username: '1', })), }; }); @@ -78,7 +78,7 @@ const renderComponent = ( ), }); -describe('Download cart table component', () => { +describe('DOI generation form component', () => { let user: ReturnType; beforeEach(() => { @@ -102,14 +102,14 @@ describe('Download cart table component', () => { ).mockResolvedValue([ { id: 1, - name: 'user1', + name: '1', fullName: 'User 1', email: 'user1@example.com', affiliation: 'Example Uni', }, { id: 2, - name: 'user2', + name: '2', fullName: 'User 2', email: 'user2@example.com', affiliation: 'Example 2 Uni', @@ -118,7 +118,7 @@ describe('Download cart table component', () => { (checkUser as jest.MockedFunction).mockResolvedValue({ id: 3, - name: 'user3', + name: '3', fullName: 'User 3', email: 'user3@example.com', affiliation: 'Example 3 Uni', @@ -156,12 +156,12 @@ describe('Download cart table component', () => { await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), - 'title' + 't' ); await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), - 'desc' + 'd' ); await user.click( @@ -237,7 +237,7 @@ describe('Download cart table component', () => { await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - 'user3' + '3' ); await user.click( @@ -254,7 +254,7 @@ describe('Download cart table component', () => { // test errors on duplicate user await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - 'user3' + '3' ); await user.click( @@ -278,7 +278,7 @@ describe('Download cart table component', () => { await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - 'user4' + '4' ); await user.click( From f60a9a49dcd68e5596f3a3e91bc6e10c7ea1d5f0 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 10:14:35 +0100 Subject: [PATCH 29/68] #1529 #1531 add e2e tests for doi minting stuff - requires some icat config changes & setting up icat-doi-minter-api on CI --- .github/add_doi_datapublicationtype.py | 13 ++ .github/config.env | 18 +++ .github/workflows/ci-build.yml | 28 +++- .../cypress/e2e/DOIGenerationForm.cy.ts | 152 ++++++++++++++++++ .../cypress/e2e/downloadCart.cy.ts | 9 ++ .../cypress/support/commands.js | 57 ++++++- .../cypress/support/index.d.ts | 17 +- .../server/e2e-settings.json | 1 + 8 files changed, 286 insertions(+), 9 deletions(-) create mode 100644 .github/add_doi_datapublicationtype.py create mode 100644 .github/config.env create mode 100644 packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts diff --git a/.github/add_doi_datapublicationtype.py b/.github/add_doi_datapublicationtype.py new file mode 100644 index 000000000..f8d896048 --- /dev/null +++ b/.github/add_doi_datapublicationtype.py @@ -0,0 +1,13 @@ +from icat.client import Client + +client = Client( + "https://localhost:8181/icat", + checkCert=False, +) +client.login("simple", {username: "root", password: "pw"}) + +data_publication_type = client.new("dataPublicationType") +data_publication_type.name = "User-defined" +data_publication_type.description = "User-defined" +data_publication_type.facility = client.get("Facility", 1) +data_publication_type.create() diff --git a/.github/config.env b/.github/config.env new file mode 100644 index 000000000..916d627c5 --- /dev/null +++ b/.github/config.env @@ -0,0 +1,18 @@ +ICAT_URL="https://localhost:8181" +FACILITY="LILS" +ICAT_USERNAME="root" +ICAT_PASSWORD="pw" +PUBLISHER="test" +MINTER_ROLE="PI" +VERSION="0.01" + +ICAT_DOI_BASE_URL="https://example.stfc.ac.uk/" +ICAT_SESSION_PATH="/icat/session/" +ICAT_AUTHENTICATOR_NAME="simple" +ICAT_CHECK_CERT=False +SSL_CERT_VERIFICATION=False + +DATACITE_PREFIX="10.5286" +DATACITE_URL="https://api.test.datacite.org/dois" +DATACITE_USERNAME="BL.STFC" + diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 7d293eebe..26e09857d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -257,9 +257,21 @@ jobs: ansible-playbook icat-ansible/icat_test_hosts.yml -i icat-ansible/hosts --vault-password-file icat-ansible/vault_pass.txt -vv # Fixes on ICAT components needed for e2e tests - - name: Add anon user to rootUserNames + - name: Removing authenticator prefix for simple auth run: | - awk -F" =" '/rootUserNames/{$2="= simple/root anon/anon";print;next}1' /home/runner/install/icat.server/run.properties > /home/runner/install/icat.server/run.properties.tmp + sed -i 's/mechanism = simple/!mechanism = simple/' /home/runner/install/authn.simple/run.properties + - name: Adding Chris481 user + run: | + sed -i '/user\.list/ s/$/ Chris481/' /home/runner/install/authn.simple/run.properties + - name: Adding Chris481 user password + run: | + echo "user.Chris481.password = pw" >> /home/runner/install/authn.simple/run.properties + - name: Reinstall authn.simple + run: | + cd /home/runner/install/authn.simple/ && ./setup -vv install + - name: Add anon, root (simple without prefix) and Chris481 users to rootUserNames + run: | + awk -F" =" '/rootUserNames/{$2="= root Chris481 anon/anon";print;next}1' /home/runner/install/icat.server/run.properties > /home/runner/install/icat.server/run.properties.tmp - name: Apply rootUserNames change run: | mv -f /home/runner/install/icat.server/run.properties.tmp /home/runner/install/icat.server/run.properties @@ -311,6 +323,18 @@ jobs: - name: Start API run: cd datagateway-api/; nohup poetry run python -m datagateway_api.src.main > api-output.txt & + # DOI minter setup + - name: Adding 'User-defined' DataPublicationType (needed for DOI minting api) + run: cd datagateway-api/; poetry run python ./.github/add_doi_datapublicationtype.py + + - name: 'Add password to env file' + run: | + echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env + cat .env + + - name: Run minting api + run: docker run -e ./.github/config.env -d -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api + # E2E tests - name: Setup Node.js uses: actions/setup-node@v3 diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts new file mode 100644 index 000000000..c02f0c3f8 --- /dev/null +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -0,0 +1,152 @@ +describe('DOI Generation form', () => { + beforeEach(() => { + cy.intercept('GET', '**/topcat/user/cart/**').as('fetchCart'); + cy.intercept('GET', '**/topcat/user/downloads**').as('fetchDownloads'); + cy.login( + { username: 'Chris481', password: 'pw', mechanism: 'simple' }, + 'Chris481' + ); + cy.clearDownloadCart(); + + cy.seedMintCart().then(() => { + cy.visit('/download').wait('@fetchCart'); + }); + }); + + afterEach(() => { + cy.clearDownloadCart(); + }); + + // tidy up the data publications table + after(() => { + cy.clearDataPublications(); + }); + + it('should be able to mint a mintable cart', () => { + cy.get('[aria-label="Calculating"]', { timeout: 20000 }).should( + 'not.exist' + ); + + cy.contains('Generate DOI').click(); + cy.url().should('include', '/download/mint'); + + cy.contains('button', 'Accept').should('be.visible'); + }); + + it('should not be able to try and mint a cart directly', () => { + cy.visit('/download/mint'); + + cy.url().should('match', /\/download$/); + }); + + describe('Form tests', () => { + beforeEach(() => { + cy.get('[aria-label="Calculating"]', { timeout: 20000 }).should( + 'not.exist' + ); + + cy.contains('Generate DOI').click(); + cy.contains('button', 'Accept').click(); + }); + + it('should not let user generate DOI when fields are still unfilled', () => { + cy.contains('button', 'Generate DOI').should('be.disabled'); + }); + + it('should let user generate DOI when fields are filled', () => { + cy.contains('DOI Title').parent().find('input').type('Test title'); + cy.contains('DOI Description') + .parent() + .find('textarea') + .first() + .type('Test description'); + + cy.contains('button', 'Generate DOI').click(); + + cy.contains('Mint Confirmation').should('be.visible'); + cy.contains('Mint was successful').should('be.visible'); + cy.contains('View Data Publication').click(); + + cy.url().should('match', /\/browse\/dataPublication\/[0-9]+$/); + }); + + it('should let user add and remove Data Publication users', () => { + cy.contains('DOI Title').parent().find('input').type('Test title'); + cy.contains('DOI Description') + .parent() + .find('textarea') + .first() + .type('Test description'); + + // wait for users to load + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + + cy.contains('Username').parent().find('input').type('Michael222'); + cy.contains('button', 'Add Creator').click(); + + // check we can't delete "ourselves" + cy.contains('Thomas') + .parent() + .contains('button', 'Delete') + .should('be.disabled'); + + cy.contains('Randy').should('be.visible'); + cy.contains('Randy').parent().contains('button', 'Delete').click(); + cy.contains('Randy').should('not.exist'); + + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + }); + + it('should not let user add invalid/duplicate Data Publication users', () => { + cy.contains('DOI Title').parent().find('input').type('Test title'); + cy.contains('DOI Description') + .parent() + .find('textarea') + .first() + .type('Test description'); + + // wait for users to load + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + + cy.get('table[aria-labelledby="creators-label"] tbody tr').should( + 'have.length', + 1 + ); + + cy.contains('Username').parent().find('input').type('Chris481'); + cy.contains('button', 'Add Creator').click(); + + cy.get('table[aria-labelledby="creators-label"] tbody tr').should( + 'have.length', + 1 + ); + cy.contains('Cannot add duplicate user').should('be.visible'); + + cy.contains('Username').parent().find('input').type('invalid'); + cy.contains('button', 'Add Creator').click(); + cy.get('table[aria-labelledby="creators-label"] tbody tr').should( + 'have.length', + 1 + ); + cy.contains('No record found: invalid in User').should('be.visible'); + + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + }); + + it('should let user see their current cart items', () => { + cy.contains('DATASET 75').should('be.visible'); + cy.get('table[aria-label="cart dataset table"] tbody tr').should( + 'have.length', + 1 + ); + + cy.contains('button', 'Datafiles').click(); + + cy.contains('Datafile 14').should('be.visible'); + cy.get('table[aria-label="cart datafile table"] tbody tr').should( + 'have.length', + 4 + ); + }); + }); +}); diff --git a/packages/datagateway-download/cypress/e2e/downloadCart.cy.ts b/packages/datagateway-download/cypress/e2e/downloadCart.cy.ts index 6d9594417..638f5ad41 100644 --- a/packages/datagateway-download/cypress/e2e/downloadCart.cy.ts +++ b/packages/datagateway-download/cypress/e2e/downloadCart.cy.ts @@ -138,4 +138,13 @@ describe('Download Cart', () => { .should('exist') .click(); }); + + it('should not be able to mint an unmintable cart', () => { + cy.get('[aria-label="Calculating"]', { timeout: 20000 }).should( + 'not.exist' + ); + + // this "button" is a link so can't actually be disabled, check pointer-events + cy.contains('Generate DOI').should('have.css', 'pointer-events', 'none'); + }); }); diff --git a/packages/datagateway-download/cypress/support/commands.js b/packages/datagateway-download/cypress/support/commands.js index d97303d1f..206ea7c2f 100644 --- a/packages/datagateway-download/cypress/support/commands.js +++ b/packages/datagateway-download/cypress/support/commands.js @@ -89,7 +89,7 @@ export const readSciGatewayToken = () => { }; }; -Cypress.Commands.add('login', (credentials) => { +Cypress.Commands.add('login', (credentials, user) => { return cy.request('datagateway-download-settings.json').then((response) => { const settings = response.body; let body = { @@ -104,7 +104,11 @@ Cypress.Commands.add('login', (credentials) => { const jwtHeader = { alg: 'HS256', typ: 'JWT' }; const payload = { sessionId: response.body.sessionID, - username: 'test', + username: user + ? user + : body.mechanism === 'anon' + ? 'anon/anon' + : 'Michael222', }; const jwt = jsrsasign.KJUR.jws.JWS.sign( 'HS256', @@ -152,6 +156,55 @@ Cypress.Commands.add('seedDownloadCart', () => { }); }); +Cypress.Commands.add('seedMintCart', () => { + const items = [ + 'dataset 75', + 'datafile 371', + 'datafile 14', + 'datafile 133', + 'datafile 252', + ].join(', '); + + return cy.request('datagateway-download-settings.json').then((response) => { + const settings = response.body; + cy.request({ + method: 'POST', + url: `${settings.downloadApiUrl}/user/cart/${settings.facilityName}/cartItems`, + body: { + sessionId: readSciGatewayToken().sessionId, + items, + }, + form: true, + }); + }); +}); + +Cypress.Commands.add('clearDataPublications', () => { + return cy.request('datagateway-download-settings.json').then((response) => { + const settings = response.body; + + cy.request({ + method: 'GET', + url: `${settings.apiUrl}/datapublications`, + headers: { Authorization: `Bearer ${readSciGatewayToken().sessionId}` }, + qs: { + where: JSON.stringify({ title: { eq: 'Test title' } }), + }, + }).then((response) => { + const datapublications = response.body; + datapublications.forEach((datapublication) => { + cy.request({ + method: 'DELETE', + url: `${settings.apiUrl}/datapublications/${datapublication.id}`, + headers: { + Authorization: `Bearer ${readSciGatewayToken().sessionId}`, + }, + }); + }); + }); + }); +}); + Cypress.Commands.add('addCartItem', (cartItem) => { return cy.request('datagateway-download-settings.json').then((response) => { const settings = response.body; diff --git a/packages/datagateway-download/cypress/support/index.d.ts b/packages/datagateway-download/cypress/support/index.d.ts index 138545f22..110365b48 100644 --- a/packages/datagateway-download/cypress/support/index.d.ts +++ b/packages/datagateway-download/cypress/support/index.d.ts @@ -1,15 +1,22 @@ declare namespace Cypress { interface Chainable { - login(credentials?: { - username: string; - password: string; - mechanism: string; - }): Cypress.Chainable; + login( + credentials?: { + username: string; + password: string; + mechanism: string; + }, + user?: string + ): Cypress.Chainable; clearDownloadCart(): Cypress.Chainable; seedDownloadCart(): Cypress.Chainable; + seedMintCart(): Cypress.Chainable; + + clearDataPublications(): Cypress.Chainable; + addCartItem(cartItem: string): Cypress.Chainable; seedDownloads(): Cypress.Chainable; diff --git a/packages/datagateway-download/server/e2e-settings.json b/packages/datagateway-download/server/e2e-settings.json index d4d9405e5..31a6b412e 100644 --- a/packages/datagateway-download/server/e2e-settings.json +++ b/packages/datagateway-download/server/e2e-settings.json @@ -3,6 +3,7 @@ "apiUrl": "http://localhost:5000", "downloadApiUrl": "https://localhost:8181/topcat", "idsUrl": "https://localhost:8181/ids", + "doiMinterUrl": "http://localhost:8000", "fileCountMax": 5000, "totalSizeMax": 1000000000000, "accessMethods": { From 5a10957337190fcfe4bc587461066a940259a271 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 10:48:04 +0100 Subject: [PATCH 30/68] Fix yaml syntax error --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 26e09857d..2e950eae3 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -257,7 +257,7 @@ jobs: ansible-playbook icat-ansible/icat_test_hosts.yml -i icat-ansible/hosts --vault-password-file icat-ansible/vault_pass.txt -vv # Fixes on ICAT components needed for e2e tests - - name: Removing authenticator prefix for simple auth + - name: Removing authenticator prefix for simple auth run: | sed -i 's/mechanism = simple/!mechanism = simple/' /home/runner/install/authn.simple/run.properties - name: Adding Chris481 user From a2a733233871281ca1a897a702b316ef8990d022 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 11:30:19 +0100 Subject: [PATCH 31/68] Fix path --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 2e950eae3..2567e5ac7 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -325,7 +325,7 @@ jobs: # DOI minter setup - name: Adding 'User-defined' DataPublicationType (needed for DOI minting api) - run: cd datagateway-api/; poetry run python ./.github/add_doi_datapublicationtype.py + run: cd datagateway-api/; poetry run python ../.github/add_doi_datapublicationtype.py - name: 'Add password to env file' run: | From a787f87e74e5fd8acb9c3c1fc919b826f2ce8d93 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 12:12:34 +0100 Subject: [PATCH 32/68] Fix python-icat code --- .github/add_doi_datapublicationtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/add_doi_datapublicationtype.py b/.github/add_doi_datapublicationtype.py index f8d896048..621802cb6 100644 --- a/.github/add_doi_datapublicationtype.py +++ b/.github/add_doi_datapublicationtype.py @@ -1,7 +1,7 @@ from icat.client import Client client = Client( - "https://localhost:8181/icat", + "https://localhost:8181", checkCert=False, ) client.login("simple", {username: "root", password: "pw"}) From dc6d86acf487046db246defa48eada76f0cd5a2c Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 12:59:32 +0100 Subject: [PATCH 33/68] Fix python syntax --- .github/add_doi_datapublicationtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/add_doi_datapublicationtype.py b/.github/add_doi_datapublicationtype.py index 621802cb6..47dd28a4d 100644 --- a/.github/add_doi_datapublicationtype.py +++ b/.github/add_doi_datapublicationtype.py @@ -4,7 +4,7 @@ "https://localhost:8181", checkCert=False, ) -client.login("simple", {username: "root", password: "pw"}) +client.login("simple", {"username": "root", "password": "pw"}) data_publication_type = client.new("dataPublicationType") data_publication_type.name = "User-defined" From 22988ffe55b12480ace4a1f3d4ecb44a0ef4e10f Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 13:38:06 +0100 Subject: [PATCH 34/68] Remove unnecessary command --- .github/workflows/ci-build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 2567e5ac7..af08c7753 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -328,9 +328,7 @@ jobs: run: cd datagateway-api/; poetry run python ../.github/add_doi_datapublicationtype.py - name: 'Add password to env file' - run: | - echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - cat .env + run: echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - name: Run minting api run: docker run -e ./.github/config.env -d -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api From 92af2ac2e841d3bb8978cecf06a64372dd552405 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 15:45:33 +0100 Subject: [PATCH 35/68] Remove quotes from config.env file --- .github/config.env | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/config.env b/.github/config.env index 916d627c5..24d2504e2 100644 --- a/.github/config.env +++ b/.github/config.env @@ -1,18 +1,18 @@ -ICAT_URL="https://localhost:8181" -FACILITY="LILS" -ICAT_USERNAME="root" -ICAT_PASSWORD="pw" -PUBLISHER="test" -MINTER_ROLE="PI" -VERSION="0.01" +ICAT_URL=https://localhost:8181 +FACILITY=LILS +ICAT_USERNAME=root +ICAT_PASSWORD=pw +PUBLISHER=test +MINTER_ROLE=PI +VERSION=0.01 -ICAT_DOI_BASE_URL="https://example.stfc.ac.uk/" -ICAT_SESSION_PATH="/icat/session/" -ICAT_AUTHENTICATOR_NAME="simple" +ICAT_DOI_BASE_URL=https://example.stfc.ac.uk/ +ICAT_SESSION_PATH=/icat/session/ +ICAT_AUTHENTICATOR_NAME=simple ICAT_CHECK_CERT=False SSL_CERT_VERIFICATION=False -DATACITE_PREFIX="10.5286" -DATACITE_URL="https://api.test.datacite.org/dois" -DATACITE_USERNAME="BL.STFC" +DATACITE_PREFIX=10.5286 +DATACITE_URL=https://api.test.datacite.org/dois +DATACITE_USERNAME=BL.STFC From be56ea1844031b3a1d64936eec90f81b11f371e1 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 31 Jul 2023 16:45:40 +0100 Subject: [PATCH 36/68] Debug docker command --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index af08c7753..7cc87c68c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -331,7 +331,7 @@ jobs: run: echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - name: Run minting api - run: docker run -e ./.github/config.env -d -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api + run: docker run -e ./.github/config.env -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api # E2E tests - name: Setup Node.js From fbe606ac4b04891e15b3112604cd6fdc8ff6e0d7 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 09:25:57 +0100 Subject: [PATCH 37/68] Fix docker env file flag --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 7cc87c68c..afa32c141 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -331,7 +331,7 @@ jobs: run: echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - name: Run minting api - run: docker run -e ./.github/config.env -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api + run: docker run --env-file ./.github/config.env -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api # E2E tests - name: Setup Node.js From cecdb1d958af71d8c256966276369407366e67ad Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 10:00:04 +0100 Subject: [PATCH 38/68] Fix docker localhost --- .github/config.env | 2 +- .github/workflows/ci-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/config.env b/.github/config.env index 24d2504e2..1e5012ea5 100644 --- a/.github/config.env +++ b/.github/config.env @@ -1,4 +1,4 @@ -ICAT_URL=https://localhost:8181 +ICAT_URL=https://host.docker.internal:8181 FACILITY=LILS ICAT_USERNAME=root ICAT_PASSWORD=pw diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index afa32c141..e769e7ae1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -331,7 +331,7 @@ jobs: run: echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - name: Run minting api - run: docker run --env-file ./.github/config.env -p 8000:8000 harbor.stfc.ac.uk/icat/doi-mint-api + run: docker run --env-file ./.github/config.env -p 8000:8000 --add-host host.docker.internal:host-gateway harbor.stfc.ac.uk/icat/doi-mint-api # E2E tests - name: Setup Node.js From e03d43be19460d00d38751a81ba384281c2be173 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 10:30:51 +0100 Subject: [PATCH 39/68] Add detached flag back in - I only removed it for debugging, as if the API is working then without -d it hangs the workflow! --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e769e7ae1..58b8f7d2a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -331,7 +331,7 @@ jobs: run: echo DATACITE_PASSWORD=${{ secrets.DATACITE_PASSWORD }} >> ./.github/config.env - name: Run minting api - run: docker run --env-file ./.github/config.env -p 8000:8000 --add-host host.docker.internal:host-gateway harbor.stfc.ac.uk/icat/doi-mint-api + run: docker run --env-file ./.github/config.env -p 8000:8000 --add-host host.docker.internal:host-gateway -d harbor.stfc.ac.uk/icat/doi-mint-api # E2E tests - name: Setup Node.js From f425b47b9c84379ee5386598e6101207865aa858 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 11:44:17 +0100 Subject: [PATCH 40/68] Debug icat config changes --- .github/workflows/ci-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 58b8f7d2a..5d7cc91a1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -266,6 +266,8 @@ jobs: - name: Adding Chris481 user password run: | echo "user.Chris481.password = pw" >> /home/runner/install/authn.simple/run.properties + - name: Debug authn.simple config + run: cat /home/runner/install/authn.simple/run.properties - name: Reinstall authn.simple run: | cd /home/runner/install/authn.simple/ && ./setup -vv install @@ -275,6 +277,8 @@ jobs: - name: Apply rootUserNames change run: | mv -f /home/runner/install/icat.server/run.properties.tmp /home/runner/install/icat.server/run.properties + - name: Debug authn.simple config + run: cat /home/runner/install/icat.server/run.properties - name: Reinstall ICAT Server run: | cd /home/runner/install/icat.server/ && ./setup -vv install From 4ed6f55a429e2950c688f0ff0310a01f2f7f7886 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 14:15:56 +0100 Subject: [PATCH 41/68] More debugging --- .github/workflows/ci-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5d7cc91a1..568a96193 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -271,13 +271,15 @@ jobs: - name: Reinstall authn.simple run: | cd /home/runner/install/authn.simple/ && ./setup -vv install + - name: Debug icat.server config + run: cat /home/runner/install/icat.server/run.properties - name: Add anon, root (simple without prefix) and Chris481 users to rootUserNames run: | awk -F" =" '/rootUserNames/{$2="= root Chris481 anon/anon";print;next}1' /home/runner/install/icat.server/run.properties > /home/runner/install/icat.server/run.properties.tmp - name: Apply rootUserNames change run: | mv -f /home/runner/install/icat.server/run.properties.tmp /home/runner/install/icat.server/run.properties - - name: Debug authn.simple config + - name: Debug icat.server config run: cat /home/runner/install/icat.server/run.properties - name: Reinstall ICAT Server run: | From f0b53d04cdc702e4957b23615d6e71fe1a30245c Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 14:41:01 +0100 Subject: [PATCH 42/68] More debugging --- .github/workflows/ci-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 568a96193..aa1a78c13 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -292,6 +292,9 @@ jobs: cd /home/runner/install/ids.server/ && python2 ./setup -vv install # Disable Globus for Download e2e tests + - name: Debug login to ICAT + run: | + curl -k --request POST 'https://localhost:8181/icat/session' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'json={"plugin":"simple", "credentials": [{"username":"root"}, {"password":"pw"}]}' - name: Login to ICAT run: | curl -k --request POST 'https://localhost:8181/icat/session' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'json={"plugin":"simple", "credentials": [{"username":"root"}, {"password":"pw"}]}' > login_output From f5591991d6ff4f02e454d2f10aad84256b38d4a8 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 15:01:08 +0100 Subject: [PATCH 43/68] Remove debugging messages & restart glassfish --- .github/workflows/ci-build.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index aa1a78c13..598f88cf9 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -266,21 +266,15 @@ jobs: - name: Adding Chris481 user password run: | echo "user.Chris481.password = pw" >> /home/runner/install/authn.simple/run.properties - - name: Debug authn.simple config - run: cat /home/runner/install/authn.simple/run.properties - name: Reinstall authn.simple run: | cd /home/runner/install/authn.simple/ && ./setup -vv install - - name: Debug icat.server config - run: cat /home/runner/install/icat.server/run.properties - name: Add anon, root (simple without prefix) and Chris481 users to rootUserNames run: | awk -F" =" '/rootUserNames/{$2="= root Chris481 anon/anon";print;next}1' /home/runner/install/icat.server/run.properties > /home/runner/install/icat.server/run.properties.tmp - name: Apply rootUserNames change run: | mv -f /home/runner/install/icat.server/run.properties.tmp /home/runner/install/icat.server/run.properties - - name: Debug icat.server config - run: cat /home/runner/install/icat.server/run.properties - name: Reinstall ICAT Server run: | cd /home/runner/install/icat.server/ && ./setup -vv install @@ -290,11 +284,10 @@ jobs: - name: Reinstall IDS Server run: | cd /home/runner/install/ids.server/ && python2 ./setup -vv install + - name: Restart glassfish (ensure new ICAT rootUserNames is applied everywhere) + run: asadmin restart-domain # Disable Globus for Download e2e tests - - name: Debug login to ICAT - run: | - curl -k --request POST 'https://localhost:8181/icat/session' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'json={"plugin":"simple", "credentials": [{"username":"root"}, {"password":"pw"}]}' - name: Login to ICAT run: | curl -k --request POST 'https://localhost:8181/icat/session' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'json={"plugin":"simple", "credentials": [{"username":"root"}, {"password":"pw"}]}' > login_output From 8d74c2564a2c5ed3f0e0056bca55788d0f11dcd9 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 15:23:01 +0100 Subject: [PATCH 44/68] Restart payara in different way - taken from how icat-ansible restarts payara --- .github/workflows/ci-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 598f88cf9..ebb6393d8 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -284,8 +284,8 @@ jobs: - name: Reinstall IDS Server run: | cd /home/runner/install/ids.server/ && python2 ./setup -vv install - - name: Restart glassfish (ensure new ICAT rootUserNames is applied everywhere) - run: asadmin restart-domain + - name: Restart payara (ensure new ICAT rootUserNames is applied everywhere) + run: /usr/local/bin/payara-init restart # Disable Globus for Download e2e tests - name: Login to ICAT From a0a33adff2884cd01962bfd78c7974822d7de356 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 15:37:37 +0100 Subject: [PATCH 45/68] Add sudo to restart payara command --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index ebb6393d8..d920695fc 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -285,7 +285,7 @@ jobs: run: | cd /home/runner/install/ids.server/ && python2 ./setup -vv install - name: Restart payara (ensure new ICAT rootUserNames is applied everywhere) - run: /usr/local/bin/payara-init restart + run: sudo /usr/local/bin/payara-init restart # Disable Globus for Download e2e tests - name: Login to ICAT From 630208f229a984969354189ac0a4757016ae0cdb Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 16:02:57 +0100 Subject: [PATCH 46/68] Fix adminUserNames in TopCAT --- .github/workflows/ci-build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d920695fc..ae14b63ed 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -284,8 +284,15 @@ jobs: - name: Reinstall IDS Server run: | cd /home/runner/install/ids.server/ && python2 ./setup -vv install - - name: Restart payara (ensure new ICAT rootUserNames is applied everywhere) - run: sudo /usr/local/bin/payara-init restart + - name: Add root (simple without prefix) to TopCAT adminUserNames + run: | + awk -F" =" '/adminUserNames/{$2="= root";print;next}1' /home/runner/install/topcat/topcat.properties > /home/runner/install/topcat/topcat.properties.tmp + - name: Apply rootUserNames change + run: | + mv -f /home/runner/install/topcat/topcat.properties.tmp /home/runner/install/topcat/topcat.properties + - name: Reinstall TopCAT + run: | + cd /home/runner/install/topcat/ && ./setup -vv install # Disable Globus for Download e2e tests - name: Login to ICAT From d3f081527d3c808cc22334afbfab1466209748f9 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Tue, 1 Aug 2023 16:18:49 +0100 Subject: [PATCH 47/68] Use python2 for Topcat reinstall --- .github/workflows/ci-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index ae14b63ed..3148e7434 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -287,12 +287,12 @@ jobs: - name: Add root (simple without prefix) to TopCAT adminUserNames run: | awk -F" =" '/adminUserNames/{$2="= root";print;next}1' /home/runner/install/topcat/topcat.properties > /home/runner/install/topcat/topcat.properties.tmp - - name: Apply rootUserNames change + - name: Apply adminUserNames change run: | mv -f /home/runner/install/topcat/topcat.properties.tmp /home/runner/install/topcat/topcat.properties - name: Reinstall TopCAT run: | - cd /home/runner/install/topcat/ && ./setup -vv install + cd /home/runner/install/topcat/ && python2 ./setup -vv install # Disable Globus for Download e2e tests - name: Login to ICAT From 00cb5edeb09402baa8ec74cecf4f2e097904fdeb Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 2 Aug 2023 09:39:00 +0100 Subject: [PATCH 48/68] Fix dataset card view e2e tests that failed on the 1st of the month --- .../datagateway-dataview/cypress/e2e/card/datasets.cy.ts | 4 ++++ .../cypress/e2e/card/dls/datasets.cy.ts | 6 +++--- .../cypress/e2e/card/isis/datasets.cy.ts | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/datagateway-dataview/cypress/e2e/card/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/datasets.cy.ts index 13bd12d23..67156b256 100644 --- a/packages/datagateway-dataview/cypress/e2e/card/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/card/datasets.cy.ts @@ -81,16 +81,20 @@ describe('Datasets Cards', () => { .click() .type('2019-01-01') .wait(['@getDatasetsCount'], { timeout: 10000 }); + cy.get('[data-testid="card"]').first().contains('DATASET 61'); cy.get('input[aria-label="Create Time filter to"]') .parent() .find('button') .click(); + cy.get('.MuiPickersCalendarHeader-label').click(); + cy.contains('2020').click(); cy.get('.MuiPickersDay-root[type="button"]') .first() .click() .wait(['@getDatasetsCount'], { timeout: 10000 }); const date = new Date(); date.setDate(1); + date.setFullYear(2020); cy.get('input[id="Create Time filter to"]').should( 'have.value', date.toISOString().slice(0, 10) diff --git a/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts index 83d3a8701..888d0dae8 100644 --- a/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts @@ -122,11 +122,11 @@ describe('DLS - Datasets Cards', () => { .wait(['@getDatasetsCount', '@getDatasetsOrder'], { timeout: 10000 }); cy.get('[data-testid="card"]').first().contains('DATASET 61'); - cy.get('input[id="Create Time filter from"]') + cy.get('input[id="Start Date filter from"]') .click() .type('2019-01-01') .wait(['@getDatasetsCount'], { timeout: 10000 }); - cy.get('input[aria-label="Create Time filter to"]') + cy.get('input[aria-label="Start Date filter to"]') .parent() .find('button') .click(); @@ -136,7 +136,7 @@ describe('DLS - Datasets Cards', () => { .wait(['@getDatasetsCount'], { timeout: 10000 }); const date = new Date(); date.setDate(1); - cy.get('input[id="Create Time filter to"]').should( + cy.get('input[id="Start Date filter to"]').should( 'have.value', date.toISOString().slice(0, 10) ); diff --git a/packages/datagateway-dataview/cypress/e2e/card/isis/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/isis/datasets.cy.ts index b28d75bb9..082d586c9 100644 --- a/packages/datagateway-dataview/cypress/e2e/card/isis/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/card/isis/datasets.cy.ts @@ -131,16 +131,20 @@ describe('ISIS - Datasets Cards', () => { .click() .type('2019-01-01') .wait(['@getDatasetsCount'], { timeout: 10000 }); + cy.get('[data-testid="card"]').first().contains('DATASET 19'); cy.get('input[aria-label="Create Time filter to"]') .parent() .find('button') .click(); + cy.get('.MuiPickersCalendarHeader-label').click(); + cy.contains('2020').click(); cy.get('.MuiPickersDay-root[type="button"]') .first() .click() .wait(['@getDatasetsCount'], { timeout: 10000 }); const date = new Date(); date.setDate(1); + date.setFullYear(2020); cy.get('input[id="Create Time filter to"]').should( 'have.value', date.toISOString().slice(0, 10) From eadd66fd79c835d041bf8d0331dcb45d1c1c8506 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 17 Aug 2023 14:49:08 +0100 Subject: [PATCH 49/68] #1531 add ability to add contributors --- .../public/res/default.json | 4 +- .../DOIGenerationForm.component.tsx | 323 +++++++++++++----- .../datagateway-download/src/downloadApi.ts | 26 +- 3 files changed, 274 insertions(+), 79 deletions(-) diff --git a/packages/datagateway-download/public/res/default.json b/packages/datagateway-download/public/res/default.json index 130d25b77..94bfbf8b9 100644 --- a/packages/datagateway-download/public/res/default.json +++ b/packages/datagateway-download/public/res/default.json @@ -107,12 +107,14 @@ "form_header": "Details", "title": "DOI Title", "description": "DOI Description", - "creators": "Creators", + "creators": "Creators & Contributors", "username": "Username", "add_creator": "Add Creator", + "add_contributor": "Add Contributor", "creator_name": "Name", "creator_affiliation": "Affiliation", "creator_email": "Email", + "creator_type": "Contributor Type", "creator_action": "Action", "delete_creator": "Delete", "generate_DOI": "Generate DOI", diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index bdd9c1b41..53ac9748b 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -2,8 +2,12 @@ import { Box, Button, CircularProgress, + FormControl, Grid, + InputLabel, + MenuItem, Paper, + Select, Tab, Table, TableBody, @@ -19,6 +23,7 @@ import { readSciGatewayToken, User } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { Redirect, useLocation } from 'react-router-dom'; +import { ContributorType } from '../downloadApi'; import { useCart, useCartUsers, @@ -28,9 +33,29 @@ import { import AcceptDataPolicy from './acceptDataPolicy.component'; import DOIConfirmDialog from './DOIConfirmDialog.component'; +type ContributorUser = User & { + contributor_type: ContributorType | ''; +}; + +const compareUsers = (a: ContributorUser, b: ContributorUser): number => { + if ( + a.contributor_type === ContributorType.Creator && + b.contributor_type !== ContributorType.Creator + ) { + return -1; + } else if ( + b.contributor_type === ContributorType.Creator && + a.contributor_type !== ContributorType.Creator + ) { + return 1; + } else return 0; +}; + const DOIGenerationForm: React.FC = () => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(false); - const [selectedUsers, setSelectedUsers] = React.useState([]); + const [selectedUsers, setSelectedUsers] = React.useState( + [] + ); const [username, setUsername] = React.useState(''); const [usernameError, setUsernameError] = React.useState(''); const [title, setTitle] = React.useState(''); @@ -58,7 +83,13 @@ const DOIGenerationForm: React.FC = () => { } = useMintCart(); React.useEffect(() => { - if (users) setSelectedUsers(users); + if (users) + setSelectedUsers( + users.map((user) => ({ + ...user, + contributor_type: ContributorType.Creator, + })) + ); }, [users]); React.useEffect(() => { @@ -267,55 +298,118 @@ const DOIGenerationForm: React.FC = () => { }} /> - - + setUsername(''); + } + }} + > + {t('DOIGenerationForm.add_creator')} + + + + + @@ -337,6 +431,9 @@ const DOIGenerationForm: React.FC = () => { {t('DOIGenerationForm.creator_email')} + + {t('DOIGenerationForm.creator_type')} + {t('DOIGenerationForm.creator_action')} @@ -353,33 +450,100 @@ const DOIGenerationForm: React.FC = () => { )} - {selectedUsers.map((user) => ( - - {user.fullName} - {user?.affiliation} - {user?.email} - - - - - ))} + } + color="secondary" + > + {t('DOIGenerationForm.delete_creator')} + + + + ))}
@@ -394,7 +558,10 @@ const DOIGenerationForm: React.FC = () => { description.length === 0 || selectedUsers.length === 0 || typeof cart === 'undefined' || - cart.length === 0 + cart.length === 0 || + selectedUsers.some( + (user) => user.contributor_type === '' + ) } onClick={() => { if (cart) { @@ -408,6 +575,8 @@ const DOIGenerationForm: React.FC = () => { ) .map((user) => ({ username: user.name, + contributor_type: + user.contributor_type as ContributorType, // we check this is true in the disabled field above })); mintCart({ cart, diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index cd3938847..1ca18f432 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -428,10 +428,34 @@ export const isCartMintable = async ( return status === 200; }; +export enum ContributorType { + Creator = 'Creator', + ContactPerson = 'ContactPerson', + DataCollector = 'DataCollector', + DataCurator = 'DataCurator', + DataManager = 'DataManager', + Distributor = 'Distributor', + Editor = 'Editor', + HostingInstitution = 'HostingInstitution', + Producer = 'Producer', + ProjectLeader = 'ProjectLeader', + ProjectManager = 'ProjectManager', + ProjectMember = 'ProjectMember', + RegistrationAgency = 'RegistrationAgency', + RelatedPerson = 'RelatedPerson', + Researcher = 'Researcher', + ResearchGroup = 'ResearchGroup', + RightsHolder = 'RightsHolder', + Sponsor = 'Sponsor', + Supervisor = 'Supervisor', + WorkPackageLeader = 'WorkPackageLeader', + Other = 'Other', +} + export interface DoiMetadata { title: string; description: string; - creators?: { username: string }[]; + creators?: { username: string; contributor_type: ContributorType }[]; } export interface DoiResult { From 4843b616c80e6b44539c2994eb00aa6b476ce7f5 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 17 Aug 2023 16:12:28 +0100 Subject: [PATCH 50/68] #1531 add unit tests for contributors feature --- .../DOIGenerationForm.component.test.tsx | 81 +++++++++- .../DOIGenerationForm.component.tsx | 149 ++++++------------ .../src/downloadApiHooks.test.tsx | 3 +- 3 files changed, 134 insertions(+), 99 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index 74a39cb21..32705b5a0 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -221,7 +221,7 @@ describe('DOI generation form component', () => { ).toBeInTheDocument(); }); - it('should let the user add users (but not duplicate users or if checkUser fails)', async () => { + it('should let the user add creators (but not duplicate users or if checkUser fails)', async () => { renderComponent(); // accept data policy @@ -250,6 +250,7 @@ describe('DOI generation form component', () => { .slice(1) // ignores the header row ).toHaveLength(3); expect(screen.getByRole('cell', { name: 'User 3' })).toBeInTheDocument(); + expect(screen.getAllByRole('cell', { name: 'Creator' }).length).toBe(3); // test errors on duplicate user await user.type( @@ -321,6 +322,84 @@ describe('DOI generation form component', () => { ).toHaveLength(3); }); + it('should let the user add contributors & select their contributor type', async () => { + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(2); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), + '3' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_contributor' }) + ); + + expect( + within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(3); + expect(screen.getByRole('cell', { name: 'User 3' })).toBeInTheDocument(); + + expect( + screen.getByRole('button', { + name: /DOIGenerationForm.creator_type/i, + }) + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { + name: /DOIGenerationForm.creator_type/i, + }) + ); + await user.click( + await screen.findByRole('option', { name: 'DataCollector' }) + ); + + expect(screen.queryByRole('option')).not.toBeInTheDocument(); + // check that the option is actually selected in the table even after the menu closes + expect(screen.getByText('DataCollector')).toBeInTheDocument(); + + // check users and their contributor types get passed correctly to API + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), + 't' + ); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), + 'd' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ); + + expect(mintCart).toHaveBeenCalledWith( + mockCartItems, + { + title: 't', + description: 'd', + creators: [ + { username: '2', contributor_type: 'Creator' }, + { username: '3', contributor_type: 'DataCollector' }, + ], + }, + expect.any(Object) + ); + }); + it('should let the user change cart tabs', async () => { renderComponent(); diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 53ac9748b..0e2aad4a0 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -107,6 +107,52 @@ const DOIGenerationForm: React.FC = () => { const [t] = useTranslation(); + /** + * Returns a function, which you pass true or false to depending on whether + * it's the creator button or not, and returns the relevant click handler + */ + const handleAddCreatorOrContributorClick = React.useCallback( + (creator: boolean) => () => { + // don't let the user add duplicates + if ( + selectedUsers.every((selectedUser) => selectedUser.name !== username) + ) { + checkUser({ throwOnError: true }) + .then((response) => { + // add user + if (response.data) { + const user: ContributorUser = { + ...response.data, + contributor_type: creator ? ContributorType.Creator : '', + }; + setSelectedUsers((selectedUsers) => [...selectedUsers, user]); + setUsername(''); + } + }) + .catch( + ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + // TODO: check this is the right message from the API + setUsernameError( + error.response?.data?.detail + ? typeof error.response.data.detail === 'string' + ? error.response.data.detail + : error.response.data.detail[0].msg + : 'Error' + ); + } + ); + } else { + setUsernameError('Cannot add duplicate user'); + setUsername(''); + } + }, + [checkUser, selectedUsers, username] + ); + // redirect if the user tries to access the link directly instead of from the cart if (!location.state?.fromCart) { return ; @@ -302,55 +348,9 @@ const DOIGenerationForm: React.FC = () => { @@ -358,54 +358,9 @@ const DOIGenerationForm: React.FC = () => { diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx index da8de8891..6fee1f194 100644 --- a/packages/datagateway-download/src/downloadApiHooks.test.tsx +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -37,6 +37,7 @@ import { } from './downloadApiHooks'; import { mockCartItems, mockDownloadItems, mockedSettings } from './testData'; import log from 'loglevel'; +import { ContributorType } from './downloadApi'; jest.mock('datagateway-common', () => { const originalModule = jest.requireActual('datagateway-common'); @@ -1490,7 +1491,7 @@ describe('Download API react-query hooks test', () => { const doiMetadata = { title: 'Test title', description: 'Test description', - creators: [{ username: '1' }], + creators: [{ username: '1', contributor_type: ContributorType.Creator }], }; it('should send a request to mint a cart', async () => { axios.post = jest.fn().mockResolvedValue({ From 8e716e65c92028941cd6a1821532424086b66ec3 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 17 Aug 2023 16:33:39 +0100 Subject: [PATCH 51/68] #1531 - add e2e test for add contributors feature --- .../cypress/e2e/DOIGenerationForm.cy.ts | 29 ++++++++++++++++++- .../DOIGenerationForm.component.tsx | 7 +++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts index c02f0c3f8..32145cfe4 100644 --- a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -70,7 +70,7 @@ describe('DOI Generation form', () => { cy.url().should('match', /\/browse\/dataPublication\/[0-9]+$/); }); - it('should let user add and remove Data Publication users', () => { + it('should let user add and remove creators', () => { cy.contains('DOI Title').parent().find('input').type('Test title'); cy.contains('DOI Description') .parent() @@ -97,6 +97,33 @@ describe('DOI Generation form', () => { cy.contains('button', 'Generate DOI').should('not.be.disabled'); }); + it('should let user add contributors and select their contributor type', () => { + cy.contains('DOI Title').parent().find('input').type('Test title'); + cy.contains('DOI Description') + .parent() + .find('textarea') + .first() + .type('Test description'); + + // wait for users to load + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + + cy.contains('Username').parent().find('input').type('Michael222'); + cy.contains('button', 'Add Contributor').click(); + + // shouldn't let users submit DOIs without selecting a contributor type + cy.contains('button', 'Generate DOI').should('be.disabled'); + + cy.contains('label', 'Contributor Type').parent().click(); + + cy.contains('DataCollector').click(); + + // check that contributor info doesn't break the API + cy.contains('button', 'Generate DOI').click(); + + cy.contains('Mint was successful').should('be.visible'); + }); + it('should not let user add invalid/duplicate Data Publication users', () => { cy.contains('DOI Title').parent().find('input').type('Test title'); cy.contains('DOI Description') diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 0e2aad4a0..5109f97b6 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -422,14 +422,15 @@ const DOIGenerationForm: React.FC = () => { size="small" required > - + {t( 'DOIGenerationForm.creator_type' )} { + setRelatedDOIs((dois) => { + return dois.map((d) => { + if ( + d.relatedIdentifier === + relatedItem.relatedIdentifier + ) { + return { + ...d, + relationType: event.target + .value as + | DOIRelationType + | '', + }; + } else { + return d; + } + }); + }); + }} + > + {Object.values(DOIRelationType).map( + (relation) => { + return ( + + {relation} + + ); + } + )} + + + + + + + {t( + 'DOIGenerationForm.related_doi_resource_type' + )} + + + + + + + + + ))} + + + + )} + + +
{ creatorsList.length > 0 ? creatorsList : undefined, + related_items: relatedDOIs, }, }); } diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index bcdc46465..4aabfefed 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -452,12 +452,90 @@ export enum ContributorType { Other = 'Other', } +export enum DOIRelationType { + IsCitedBy = 'IsCitedBy', + Cites = 'Cites', + IsSupplementTo = 'IsSupplementTo', + IsSupplementedBy = 'IsSupplementedBy', + IsContinuedBy = 'IsContinuedBy', + Continues = 'Continues', + IsDescribedBy = 'IsDescribedBy', + Describes = 'Describes', + HasMetadata = 'HasMetadata', + IsMetadataFor = 'IsMetadataFor', + HasVersion = 'HasVersion', + IsVersionOf = 'IsVersionOf', + IsNewVersionOf = 'IsNewVersionOf', + IsPreviousVersionOf = 'IsPreviousVersionOf', + IsPartOf = 'IsPartOf', + HasPart = 'HasPart', + IsPublishedIn = 'IsPublishedIn', + IsReferencedBy = 'IsReferencedBy', + References = 'References', + IsDocumentedBy = 'IsDocumentedBy', + Documents = 'Documents', + IsCompiledBy = 'IsCompiledBy', + Compiles = 'Compiles', + IsVariantFormOf = 'IsVariantFormOf', + IsOriginalFormOf = 'IsOriginalFormOf', + IsIdenticalTo = 'IsIdenticalTo', + IsReviewedBy = 'IsReviewedBy', + Reviews = 'Reviews', + IsDerivedFrom = 'IsDerivedFrom', + IsSourceOf = 'IsSourceOf', + IsRequiredBy = 'IsRequiredBy', + Requires = 'Requires', + Obsoletes = 'Obsoletes', + IsObsoletedBy = 'IsObsoletedBy', +} + +export enum DOIResourceType { + Audiovisual = 'Audiovisual', + Book = 'Book', + BookChapter = 'BookChapter', + Collection = 'Collection', + ComputationalNotebook = 'ComputationalNotebook', + ConferencePaper = 'ConferencePaper', + ConferenceProceeding = 'ConferenceProceeding', + DataPaper = 'DataPaper', + Dataset = 'Dataset', + Dissertation = 'Dissertation', + Event = 'Event', + Image = 'Image', + InteractiveResource = 'InteractiveResource', + Journal = 'Journal', + JournalArticle = 'JournalArticle', + Model = 'Model', + OutputManagementPlan = 'OutputManagementPlan', + PeerReview = 'PeerReview', + PhysicalObject = 'PhysicalObject', + Preprint = 'Preprint', + Report = 'Report', + Service = 'Service', + Software = 'Software', + Sound = 'Sound', + Standard = 'Standard', + Text = 'Text', + Workflow = 'Workflow', + Other = 'Other', +} + export interface DoiMetadata { title: string; description: string; creators?: { username: string; contributor_type: ContributorType }[]; + related_items: RelatedDOI[]; } +export type RelatedDOI = { + title: string; + fullReference: string; + relatedIdentifier: string; + relatedIdentifierType: 'DOI'; + relationType: DOIRelationType | ''; + resourceType: DOIResourceType | ''; +}; + export interface DoiResponse { concept: DoiResult; version: DoiResult; @@ -492,7 +570,6 @@ export const mintCart = ( metadata: { ...doiMetadata, resource_type: investigations.length === 0 ? 'Dataset' : 'Collection', - related_items: [], }, ...(investigations.length > 0 ? { investigations: { ids: investigations } } @@ -615,3 +692,31 @@ export const checkUser = ( return response.data; }); }; + +interface DataCiteResponse { + data: DataCiteDOI; +} + +export interface DataCiteDOI { + id: string; + type: string; + attributes: { + doi: string; + titles: { title: string }[]; + url: string; + }; +} +/** + * Retrieve metadata for a DOI + * @param doi The DOI to fetch metadata for + */ +export const fetchDOI = ( + doi: string, + settings: Pick +): Promise => { + return axios + .get(`${settings.dataCiteUrl}/dois/${doi}`) + .then((response) => { + return response.data.data; + }); +}; diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index fbf9dc561..53001c8f8 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -37,9 +37,11 @@ import { DoiResponse, DownloadProgress, DownloadTypeStatus, + fetchDOI, getCartUsers, isCartMintable, mintCart, + RelatedDOI, SubmitCartZipType, } from './downloadApi'; import { @@ -952,3 +954,37 @@ export const useCheckUser = ( } ); }; + +/** + * Checks whether a DOI is valid and returns the DOI metadata + * @param doi The DOI that we're checking + * @returns the {@link RelatedDOI} that matches the username, or 404 + */ +export const useCheckDOI = ( + doi: string +): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + + return useQuery(['checkDOI', doi], () => fetchDOI(doi, settings), { + retry: (failureCount: number, error: AxiosError) => { + if ( + // DOI is invalid - don't retry as this is a correct response from the server + error.response?.status === 404 || + failureCount >= 3 + ) + return false; + return true; + }, + select: (doi) => ({ + title: doi.attributes.titles[0].title, + relatedIdentifier: doi.attributes.doi, + relatedIdentifierType: 'DOI', + fullReference: '', // TODO: what should we put here? + relationType: '', + resourceType: '', + }), + // set enabled false to only fetch on demand when the add creator button is pressed + enabled: false, + cacheTime: 0, + }); +}; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index 64e93925a..f19e540f3 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -534,7 +534,7 @@ const DownloadCartTable: React.FC = ( {t('downloadCart.remove_all')} - {settings.doiMinterUrl && ( + {settings.doiMinterUrl && settings.dataCiteUrl && ( Date: Wed, 1 Nov 2023 13:21:49 +0000 Subject: [PATCH 59/68] #1531 - extract out users & related dois components & write unit tests for related DOIs --- .../DOIGenerationForm.component.test.tsx | 284 ++++----- .../DOIGenerationForm.component.tsx | 600 +----------------- ...creatorsAndContributors.component.test.tsx | 278 ++++++++ .../creatorsAndContributors.component.tsx | 282 ++++++++ .../relatedDOIs.component.test.tsx | 192 ++++++ .../relatedDOIs.component.tsx | 266 ++++++++ 6 files changed, 1173 insertions(+), 729 deletions(-) create mode 100644 packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.test.tsx create mode 100644 packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.tsx create mode 100644 packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx create mode 100644 packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index 32705b5a0..cbda13d91 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -15,6 +15,9 @@ import { DownloadSettingsContext } from '../ConfigProvider'; import { mockCartItems, mockedSettings } from '../testData'; import { checkUser, + DOIRelationType, + DOIResourceType, + fetchDOI, getCartUsers, isCartMintable, mintCart, @@ -34,9 +37,6 @@ jest.mock('datagateway-common', () => { __esModule: true, ...originalModule, fetchDownloadCart: jest.fn(), - readSciGatewayToken: jest.fn(() => ({ - username: '1', - })), }; }); @@ -49,6 +49,7 @@ jest.mock('../downloadApi', () => { getCartUsers: jest.fn(), checkUser: jest.fn(), mintCart: jest.fn(), + fetchDOI: jest.fn(), }; }); @@ -107,21 +108,24 @@ describe('DOI generation form component', () => { email: 'user1@example.com', affiliation: 'Example Uni', }, - { - id: 2, - name: '2', - fullName: 'User 2', - email: 'user2@example.com', - affiliation: 'Example 2 Uni', - }, ]); (checkUser as jest.MockedFunction).mockResolvedValue({ - id: 3, - name: '3', - fullName: 'User 3', - email: 'user3@example.com', - affiliation: 'Example 3 Uni', + id: 2, + name: '2', + fullName: 'User 2', + email: 'user2@example.com', + affiliation: 'Example 2 Uni', + }); + + (fetchDOI as jest.MockedFunction).mockResolvedValue({ + id: '1', + type: 'DOI', + attributes: { + doi: 'related.doi.1', + titles: [{ title: 'Related DOI 1' }], + url: 'www.example.com', + }, }); }); @@ -187,7 +191,7 @@ describe('DOI generation form component', () => { ); }); - it('should let the user delete users (but not delete the logged in user)', async () => { + it('should not let the user submit a mint request if required fields are missing but can submit once all are filled in', async () => { renderComponent(); // accept data policy @@ -195,134 +199,116 @@ describe('DOI generation form component', () => { screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) ); + // missing title expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(2); - expect( - screen.getByRole('cell', { name: 'user2@example.com' }) - ).toBeInTheDocument(); - - const userDeleteButtons = screen.getAllByRole('button', { - name: 'DOIGenerationForm.delete_creator', - }); - expect(userDeleteButtons[0]).toBeDisabled(); - - await user.click(userDeleteButtons[1]); - - expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) - ).toHaveLength(1); - expect( - screen.getByRole('cell', { name: 'Example Uni' }) - ).toBeInTheDocument(); - }); - - it('should let the user add creators (but not duplicate users or if checkUser fails)', async () => { - renderComponent(); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); - // accept data policy - await user.click( - screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), + 't' ); + // missing description expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(2); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); await user.type( - screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - '3' + screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), + 'd' ); - await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) - ); + // missing cart users - expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); - expect(screen.getByRole('cell', { name: 'User 3' })).toBeInTheDocument(); - expect(screen.getAllByRole('cell', { name: 'Creator' }).length).toBe(3); - - // test errors on duplicate user await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - '3' + '2' ); await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + screen.getByRole('button', { name: 'DOIGenerationForm.add_contributor' }) ); + // missing contributor type expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); - expect(screen.getByText('Cannot add duplicate user')).toBeInTheDocument(); - expect( - screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }) - ).toHaveValue(''); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); - // test errors with various API error responses - (checkUser as jest.MockedFunction).mockRejectedValueOnce({ - response: { data: { detail: 'error msg' }, status: 404 }, - }); + await user.click( + screen.getByRole('button', { + name: /DOIGenerationForm.creator_type/i, + }) + ); + await user.click( + await screen.findByRole('option', { name: 'DataCollector' }) + ); await user.type( - screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - '4' + screen.getByRole('textbox', { name: 'DOIGenerationForm.related_doi' }), + '1' ); await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + screen.getByRole('button', { name: 'DOIGenerationForm.add_related_doi' }) ); - expect(await screen.findByText('error msg')).toBeInTheDocument(); + // missing relationship type expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); - (checkUser as jest.MockedFunction).mockRejectedValue({ - response: { data: { detail: [{ msg: 'error msg 2' }] }, status: 404 }, - }); await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + screen.getByRole('button', { + name: /DOIGenerationForm.related_doi_relationship/i, + }) ); + await user.click(await screen.findByRole('option', { name: 'IsCitedBy' })); - expect(await screen.findByText('error msg 2')).toBeInTheDocument(); + // missing resource type expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); - (checkUser as jest.MockedFunction).mockRejectedValueOnce({ - response: { status: 422 }, - }); await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + screen.getByRole('button', { + name: /DOIGenerationForm.related_doi_resource_type/i, + }) ); + await user.click(await screen.findByRole('option', { name: 'Journal' })); - expect(await screen.findByText('Error')).toBeInTheDocument(); - expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ); + + expect(mintCart).toHaveBeenCalledWith( + mockCartItems, + { + title: 't', + description: 'd', + creators: [ + { username: '1', contributor_type: 'Creator' }, + { username: '2', contributor_type: 'DataCollector' }, + ], + related_items: [ + { + title: 'Related DOI 1', + fullReference: '', + relatedIdentifier: 'related.doi.1', + relatedIdentifierType: 'DOI', + relationType: DOIRelationType.IsCitedBy, + resourceType: DOIResourceType.Journal, + }, + ], + }, + expect.any(Object) + ); }); - it('should let the user add contributors & select their contributor type', async () => { + it('should not let the user submit a mint request if cart fails to load', async () => { + ( + fetchDownloadCart as jest.MockedFunction + ).mockRejectedValue({ message: 'error' }); renderComponent(); // accept data policy @@ -330,48 +316,33 @@ describe('DOI generation form component', () => { screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) ); - expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(2); - await user.type( - screen.getByRole('textbox', { name: 'DOIGenerationForm.username' }), - '3' + screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), + 't' ); - await user.click( - screen.getByRole('button', { name: 'DOIGenerationForm.add_contributor' }) + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), + 'd' ); + // missing cart expect( - within(screen.getByRole('table', { name: 'DOIGenerationForm.creators' })) - .getAllByRole('row') - .slice(1) // ignores the header row - ).toHaveLength(3); - expect(screen.getByRole('cell', { name: 'User 3' })).toBeInTheDocument(); + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); + }); - expect( - screen.getByRole('button', { - name: /DOIGenerationForm.creator_type/i, - }) - ).toBeInTheDocument(); + it('should not let the user submit a mint request if cart is empty', async () => { + ( + fetchDownloadCart as jest.MockedFunction + ).mockResolvedValue([]); + renderComponent(); + // accept data policy await user.click( - screen.getByRole('button', { - name: /DOIGenerationForm.creator_type/i, - }) - ); - await user.click( - await screen.findByRole('option', { name: 'DataCollector' }) + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) ); - expect(screen.queryByRole('option')).not.toBeInTheDocument(); - // check that the option is actually selected in the table even after the menu closes - expect(screen.getByText('DataCollector')).toBeInTheDocument(); - - // check users and their contributor types get passed correctly to API await user.type( screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), 't' @@ -382,22 +353,37 @@ describe('DOI generation form component', () => { 'd' ); - await user.click( + // empty cart + expect( screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); + }); + + it('should not let the user submit a mint request if no users selected', async () => { + ( + getCartUsers as jest.MockedFunction + ).mockResolvedValue([]); + renderComponent(); + + // accept data policy + await user.click( + screen.getByRole('button', { name: 'acceptDataPolicy.accept' }) ); - expect(mintCart).toHaveBeenCalledWith( - mockCartItems, - { - title: 't', - description: 'd', - creators: [ - { username: '2', contributor_type: 'Creator' }, - { username: '3', contributor_type: 'DataCollector' }, - ], - }, - expect.any(Object) + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.title' }), + 't' ); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.description' }), + 'd' + ); + + // no users + expect( + screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) + ).toBeDisabled(); }); it('should let the user change cart tabs', async () => { diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index d3e305dd4..663243d5d 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -1,13 +1,8 @@ import { Box, Button, - CircularProgress, - FormControl, Grid, - InputLabel, - MenuItem, Paper, - Select, Tab, Table, TableBody, @@ -18,44 +13,18 @@ import { TextField, Typography, } from '@mui/material'; -import { AxiosError } from 'axios'; -import { readSciGatewayToken, User } from 'datagateway-common'; +import { readSciGatewayToken } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { Redirect, useLocation } from 'react-router-dom'; -import { - ContributorType, - DOIRelationType, - DOIResourceType, - type RelatedDOI, -} from '../downloadApi'; -import { - useCart, - useCartUsers, - useCheckDOI, - useCheckUser, - useMintCart, -} from '../downloadApiHooks'; +import { ContributorType, type RelatedDOI } from '../downloadApi'; +import { useCart, useCartUsers, useMintCart } from '../downloadApiHooks'; import AcceptDataPolicy from './acceptDataPolicy.component'; +import CreatorsAndContributors, { + ContributorUser, +} from './creatorsAndContributors.component'; import DOIConfirmDialog from './DOIConfirmDialog.component'; - -type ContributorUser = User & { - contributor_type: ContributorType | ''; -}; - -const compareUsers = (a: ContributorUser, b: ContributorUser): number => { - if ( - a.contributor_type === ContributorType.Creator && - b.contributor_type !== ContributorType.Creator - ) { - return -1; - } else if ( - b.contributor_type === ContributorType.Creator && - a.contributor_type !== ContributorType.Creator - ) { - return 1; - } else return 0; -}; +import RelatedDOIs from './relatedDOIs.component'; const DOIGenerationForm: React.FC = () => { const [acceptedDataPolicy, setAcceptedDataPolicy] = React.useState(false); @@ -63,10 +32,6 @@ const DOIGenerationForm: React.FC = () => { [] ); const [relatedDOIs, setRelatedDOIs] = React.useState([]); - const [username, setUsername] = React.useState(''); - const [usernameError, setUsernameError] = React.useState(''); - const [relatedDOI, setRelatedDOI] = React.useState(''); - const [relatedDOIError, setRelatedDOIError] = React.useState(''); const [title, setTitle] = React.useState(''); const [description, setDescription] = React.useState(''); const [currentTab, setCurrentTab] = React.useState< @@ -83,8 +48,6 @@ const DOIGenerationForm: React.FC = () => { const { data: cart } = useCart(); const { data: users } = useCartUsers(cart); - const { refetch: checkUser } = useCheckUser(username); - const { refetch: checkDOI } = useCheckDOI(relatedDOI); const { mutate: mintCart, status: mintingStatus, @@ -117,52 +80,6 @@ const DOIGenerationForm: React.FC = () => { const [t] = useTranslation(); - /** - * Returns a function, which you pass true or false to depending on whether - * it's the creator button or not, and returns the relevant click handler - */ - const handleAddCreatorOrContributorClick = React.useCallback( - (creator: boolean) => () => { - // don't let the user add duplicates - if ( - selectedUsers.every((selectedUser) => selectedUser.name !== username) - ) { - checkUser({ throwOnError: true }) - .then((response) => { - // add user - if (response.data) { - const user: ContributorUser = { - ...response.data, - contributor_type: creator ? ContributorType.Creator : '', - }; - setSelectedUsers((selectedUsers) => [...selectedUsers, user]); - setUsername(''); - } - }) - .catch( - ( - error: AxiosError<{ - detail: { msg: string }[] | string; - }> - ) => { - // TODO: check this is the right message from the API - setUsernameError( - error.response?.data?.detail - ? typeof error.response.data.detail === 'string' - ? error.response.data.detail - : error.response.data.detail[0].msg - : 'Error' - ); - } - ); - } else { - setUsernameError('Cannot add duplicate user'); - setUsername(''); - } - }, - [checkUser, selectedUsers, username] - ); - // redirect if the user tries to access the link directly instead of from the cart if (!location.state?.fromCart) { return ; @@ -295,498 +212,16 @@ const DOIGenerationForm: React.FC = () => { /> - - theme.palette.mode === 'dark' - ? theme.palette.grey[800] - : theme.palette.grey[100], - padding: 1, - }} - elevation={0} - variant="outlined" - > - - - - {t('DOIGenerationForm.related_dois')} - - - 0 ? 2 : 0, - }} - > - - 0} - helperText={ - relatedDOIError.length > 0 - ? relatedDOIError - : '' - } - color="secondary" - sx={{ - // this CSS makes it so that the helperText doesn't mess with the button alignment - '& .MuiFormHelperText-root': { - position: 'absolute', - bottom: '-1.5rem', - }, - }} - InputProps={{ - sx: { - backgroundColor: 'background.default', - }, - }} - value={relatedDOI} - onChange={(event) => { - setRelatedDOI(event.target.value); - setRelatedDOIError(''); - }} - /> - - - - - - {relatedDOIs.length > 0 && ( - - - - - - {t('DOIGenerationForm.related_doi_doi')} - - - {t( - 'DOIGenerationForm.related_doi_relationship' - )} - - - {t( - 'DOIGenerationForm.related_doi_resource_type' - )} - - - {t('DOIGenerationForm.related_doi_action')} - - - - - {relatedDOIs.map((relatedItem) => ( - - - {relatedItem.relatedIdentifier} - - - - - {t( - 'DOIGenerationForm.related_doi_relationship' - )} - - - - - - - - {t( - 'DOIGenerationForm.related_doi_resource_type' - )} - - - - - - - - - ))} - -
-
- )} -
-
+
- - theme.palette.mode === 'dark' - ? theme.palette.grey[800] - : theme.palette.grey[100], - padding: 1, - }} - elevation={0} - variant="outlined" - > - - - - {t('DOIGenerationForm.creators')} - - - 0 ? 2 : 0, - }} - > - - 0} - helperText={ - usernameError.length > 0 ? usernameError : '' - } - color="secondary" - sx={{ - // this CSS makes it so that the helperText doesn't mess with the button alignment - '& .MuiFormHelperText-root': { - position: 'absolute', - bottom: '-1.5rem', - }, - }} - InputProps={{ - sx: { - backgroundColor: 'background.default', - }, - }} - value={username} - onChange={(event) => { - setUsername(event.target.value); - setUsernameError(''); - }} - /> - - - - - - - - - - - - - - - - {t('DOIGenerationForm.creator_name')} - - - {t('DOIGenerationForm.creator_affiliation')} - - - {t('DOIGenerationForm.creator_email')} - - - {t('DOIGenerationForm.creator_type')} - - - {t('DOIGenerationForm.creator_action')} - - - - - {typeof users === 'undefined' && ( - - - - - - )} - {[...selectedUsers] // need to spread so we don't alter underlying array - .sort(compareUsers) - .map((user) => ( - - {user.fullName} - {user?.affiliation} - {user?.email} - - {user.contributor_type === - ContributorType.Creator ? ( - ContributorType.Creator - ) : ( - - - {t( - 'DOIGenerationForm.creator_type' - )} - - - - )} - - - - - - ))} - -
-
-
-
+
+ + + + +
+ + + + + + {t('DOIGenerationForm.creator_name')} + + {t('DOIGenerationForm.creator_affiliation')} + + {t('DOIGenerationForm.creator_email')} + {t('DOIGenerationForm.creator_type')} + {t('DOIGenerationForm.creator_action')} + + + + {selectedUsers.length === 0 && ( + + + + + + )} + {[...selectedUsers] // need to spread so we don't alter underlying array + .sort(compareUsers) + .map((user) => ( + + {user.fullName} + {user?.affiliation} + {user?.email} + + {user.contributor_type === ContributorType.Creator ? ( + ContributorType.Creator + ) : ( + + + {t('DOIGenerationForm.creator_type')} + + + + )} + + + + + + ))} + +
+
+ + + ); +}; + +export default CreatorsAndContributors; diff --git a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx new file mode 100644 index 000000000..35bf5eb5f --- /dev/null +++ b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx @@ -0,0 +1,192 @@ +import { render, RenderResult, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import { QueryClient, QueryClientProvider, setLogger } from 'react-query'; +import { DownloadSettingsContext } from '../ConfigProvider'; +import { mockedSettings } from '../testData'; +import { fetchDOI } from '../downloadApi'; +import RelatedDOIs from './relatedDOIs.component'; + +setLogger({ + log: console.log, + warn: console.warn, + error: jest.fn(), +}); + +jest.mock('../downloadApi', () => { + const originalModule = jest.requireActual('../downloadApi'); + + return { + ...originalModule, + + fetchDOI: jest.fn(), + }; +}); + +const createTestQueryClient = (): QueryClient => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + +describe('DOI generation form component', () => { + let user: ReturnType; + + let props: React.ComponentProps; + + const TestComponent: React.FC = () => { + const [relatedDOIs, changeRelatedDOIs] = React.useState( + // eslint-disable-next-line react/prop-types + props.relatedDOIs + ); + + return ( + + + + + + ); + }; + + const renderComponent = (): RenderResult => render(); + + beforeEach(() => { + user = userEvent.setup(); + + props = { + relatedDOIs: [ + { + title: 'Related DOI 1', + fullReference: '', + relatedIdentifier: 'related.doi.1', + relatedIdentifierType: 'DOI', + relationType: '', + resourceType: '', + }, + ], + changeRelatedDOIs: jest.fn(), + }; + (fetchDOI as jest.MockedFunction).mockResolvedValue({ + id: '2', + type: 'DOI', + attributes: { + doi: 'related.doi.2', + titles: [{ title: 'Related DOI 2' }], + url: 'www.example.com', + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should let the user add related dois (but not if fetchDOI fails) + lets you change the relation type + resource type', async () => { + renderComponent(); + + expect( + within( + screen.getByRole('table', { name: 'DOIGenerationForm.related_dois' }) + ) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(1); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.related_doi' }), + '2' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_related_doi' }) + ); + + expect( + within( + screen.getByRole('table', { name: 'DOIGenerationForm.related_dois' }) + ) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(2); + expect( + screen.getByRole('cell', { name: 'related.doi.2' }) + ).toBeInTheDocument(); + + await user.click( + screen.getAllByRole('button', { + name: /DOIGenerationForm.related_doi_relationship/i, + })[0] + ); + await user.click(await screen.findByRole('option', { name: 'IsCitedBy' })); + + expect(screen.queryByRole('option')).not.toBeInTheDocument(); + // check that the option is actually selected in the table even after the menu closes + expect(screen.getByText('IsCitedBy')).toBeInTheDocument(); + + await user.click( + screen.getAllByRole('button', { + name: /DOIGenerationForm.related_doi_resource_type/i, + })[0] + ); + await user.click(await screen.findByRole('option', { name: 'Journal' })); + + expect(screen.queryByRole('option')).not.toBeInTheDocument(); + // check that the option is actually selected in the table even after the menu closes + expect(screen.getByText('Journal')).toBeInTheDocument(); + + // test errors with various API error responses + (fetchDOI as jest.MockedFunction).mockRejectedValueOnce({ + response: { data: { errors: [{ title: 'error msg' }] }, status: 404 }, + }); + + await user.type( + screen.getByRole('textbox', { name: 'DOIGenerationForm.related_doi' }), + '3' + ); + + await user.click( + screen.getByRole('button', { name: 'DOIGenerationForm.add_related_doi' }) + ); + + expect(await screen.findByText('error msg')).toBeInTheDocument(); + expect( + within( + screen.getByRole('table', { name: 'DOIGenerationForm.related_dois' }) + ) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(2); + }); + + it('should let the user delete related dois', async () => { + renderComponent(); + + expect( + within( + screen.getByRole('table', { name: 'DOIGenerationForm.related_dois' }) + ) + .getAllByRole('row') + .slice(1) // ignores the header row + ).toHaveLength(1); + expect( + screen.getByRole('cell', { name: 'related.doi.1' }) + ).toBeInTheDocument(); + + await user.click( + screen.getByRole('button', { + name: 'DOIGenerationForm.delete_related_doi', + }) + ); + + expect( + screen.queryByRole('table', { name: 'DOIGenerationForm.related_dois' }) + ).not.toBeInTheDocument(); + }); +}); diff --git a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx new file mode 100644 index 000000000..5817d0cb5 --- /dev/null +++ b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx @@ -0,0 +1,266 @@ +import { + Button, + FormControl, + Grid, + InputLabel, + MenuItem, + Paper, + Select, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import { AxiosError } from 'axios'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { DOIRelationType, DOIResourceType, RelatedDOI } from '../downloadApi'; +import { useCheckDOI } from '../downloadApiHooks'; + +type RelatedDOIsProps = { + relatedDOIs: RelatedDOI[]; + changeRelatedDOIs: React.Dispatch>; +}; + +const RelatedDOIs: React.FC = (props) => { + const { relatedDOIs, changeRelatedDOIs } = props; + const [t] = useTranslation(); + const [relatedDOI, setRelatedDOI] = React.useState(''); + const [relatedDOIError, setRelatedDOIError] = React.useState(''); + const { refetch: checkDOI } = useCheckDOI(relatedDOI); + + return ( + + theme.palette.mode === 'dark' + ? theme.palette.grey[800] + : theme.palette.grey[100], + padding: 1, + }} + elevation={0} + variant="outlined" + > + + + + {t('DOIGenerationForm.related_dois')} + + + 0 ? 2 : 0, + }} + > + + 0} + helperText={relatedDOIError.length > 0 ? relatedDOIError : ''} + color="secondary" + sx={{ + // this CSS makes it so that the helperText doesn't mess with the button alignment + '& .MuiFormHelperText-root': { + position: 'absolute', + bottom: '-1.5rem', + }, + }} + InputProps={{ + sx: { + backgroundColor: 'background.default', + }, + }} + value={relatedDOI} + onChange={(event) => { + setRelatedDOI(event.target.value); + setRelatedDOIError(''); + }} + /> + + + + + + {relatedDOIs.length > 0 && ( + + + + + + {t('DOIGenerationForm.related_doi_doi')} + + + {t('DOIGenerationForm.related_doi_relationship')} + + + {t('DOIGenerationForm.related_doi_resource_type')} + + + {t('DOIGenerationForm.related_doi_action')} + + + + + {relatedDOIs.map((relatedItem) => ( + + {relatedItem.relatedIdentifier} + + + + {t('DOIGenerationForm.related_doi_relationship')} + + + + + + + + {t('DOIGenerationForm.related_doi_resource_type')} + + + + + + + + + ))} + +
+
+ )} +
+
+ ); +}; + +export default RelatedDOIs; From 32f4ab68f5425f5a43bff6b0e96c2a51ef896b09 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 2 Nov 2023 11:27:43 +0000 Subject: [PATCH 60/68] #1531 - add e2e test for related DOIs --- .../cypress/e2e/DOIGenerationForm.cy.ts | 35 +++++++++++++++++++ .../server/e2e-settings.json | 1 + 2 files changed, 36 insertions(+) diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts index 32145cfe4..20e076800 100644 --- a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -160,6 +160,41 @@ describe('DOI Generation form', () => { cy.contains('button', 'Generate DOI').should('not.be.disabled'); }); + it('should let user add related DOIs and select their relation & resource type', () => { + cy.contains('DOI Title').parent().find('input').type('Test title'); + cy.contains('DOI Description') + .parent() + .find('textarea') + .first() + .type('Test description'); + + // wait for users to load + cy.contains('button', 'Generate DOI').should('not.be.disabled'); + + // DOI from https://support.datacite.org/docs/testing-guide + cy.contains(/^DOI$/).parent().find('input').type('10.17596/w76y-4s92'); + cy.contains('button', 'Add DOI').click(); + + // shouldn't let users submit DOIs without selecting a relation or resource type + cy.contains('button', 'Generate DOI').should('be.disabled'); + + cy.contains('label', 'Resource Type').parent().click(); + + cy.contains('Journal').click(); + + // shouldn't let users submit DOIs without selecting a relation type + cy.contains('button', 'Generate DOI').should('be.disabled'); + + cy.contains('label', 'Relationship').parent().click(); + + cy.contains('IsCitedBy').click(); + + // check that related DOIs info doesn't break the API + cy.contains('button', 'Generate DOI').click(); + + cy.contains('Mint was successful').should('be.visible'); + }); + it('should let user see their current cart items', () => { cy.contains('DATASET 75').should('be.visible'); cy.get('table[aria-label="cart dataset table"] tbody tr').should( diff --git a/packages/datagateway-download/server/e2e-settings.json b/packages/datagateway-download/server/e2e-settings.json index 31a6b412e..676dd62aa 100644 --- a/packages/datagateway-download/server/e2e-settings.json +++ b/packages/datagateway-download/server/e2e-settings.json @@ -4,6 +4,7 @@ "downloadApiUrl": "https://localhost:8181/topcat", "idsUrl": "https://localhost:8181/ids", "doiMinterUrl": "http://localhost:8000", + "dataCiteUrl": "https://api.test.datacite.org", "fileCountMax": 5000, "totalSizeMax": 1000000000000, "accessMethods": { From ab61bd3e4ffaf17fc7becda70fd08a5a2366aaae Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 2 Nov 2023 11:47:52 +0000 Subject: [PATCH 61/68] #1531 - render related DOIs as links + show title on hover --- .../relatedDOIs.component.test.tsx | 14 ++++++++++++++ .../DOIGenerationForm/relatedDOIs.component.tsx | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx index 35bf5eb5f..c166bbe3f 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.test.tsx @@ -189,4 +189,18 @@ describe('DOI generation form component', () => { screen.queryByRole('table', { name: 'DOIGenerationForm.related_dois' }) ).not.toBeInTheDocument(); }); + + it('should render dois as links and show title on hover', async () => { + renderComponent(); + + const doiLink = screen.getByRole('link', { name: 'related.doi.1' }); + + expect(doiLink).toHaveAttribute('href', 'https://doi.org/related.doi.1'); + + await user.hover(doiLink); + + expect( + await screen.findByRole('tooltip', { name: 'Related DOI 1' }) + ).toBeInTheDocument(); + }); }); diff --git a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx index 5817d0cb5..c3716b3e4 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx @@ -3,6 +3,7 @@ import { FormControl, Grid, InputLabel, + Link, MenuItem, Paper, Select, @@ -15,6 +16,7 @@ import { Typography, } from '@mui/material'; import { AxiosError } from 'axios'; +import { StyledTooltip } from 'datagateway-common/lib/arrowtooltip.component'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { DOIRelationType, DOIResourceType, RelatedDOI } from '../downloadApi'; @@ -148,7 +150,15 @@ const RelatedDOIs: React.FC = (props) => { {relatedDOIs.map((relatedItem) => ( - {relatedItem.relatedIdentifier} + + + + {relatedItem.relatedIdentifier} + + + Date: Thu, 2 Nov 2023 12:03:20 +0000 Subject: [PATCH 62/68] #1531 - set minWidths for selects & disable add user buttons when cart users haven't loaded yet --- .../DOIGenerationForm.component.test.tsx | 8 ++++++++ .../creatorsAndContributors.component.tsx | 9 ++++++++- .../DOIGenerationForm/relatedDOIs.component.tsx | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index cbda13d91..01128ce50 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -384,6 +384,14 @@ describe('DOI generation form component', () => { expect( screen.getByRole('button', { name: 'DOIGenerationForm.generate_DOI' }) ).toBeDisabled(); + + // expect add user + add contributor buttons to also be disabled + expect( + screen.getByRole('button', { name: 'DOIGenerationForm.add_creator' }) + ).toBeDisabled(); + expect( + screen.getByRole('button', { name: 'DOIGenerationForm.add_contributor' }) + ).toBeDisabled(); }); it('should let the user change cart tabs', async () => { diff --git a/packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.tsx index 87ab13193..5cca13279 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/creatorsAndContributors.component.tsx @@ -159,6 +159,7 @@ const CreatorsAndContributors: React.FC = ( @@ -167,6 +168,7 @@ const CreatorsAndContributors: React.FC = ( @@ -211,7 +213,12 @@ const CreatorsAndContributors: React.FC = ( {user.contributor_type === ContributorType.Creator ? ( ContributorType.Creator ) : ( - + diff --git a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx index c3716b3e4..9994a1ad4 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/relatedDOIs.component.tsx @@ -160,7 +160,12 @@ const RelatedDOIs: React.FC = (props) => { - + @@ -203,7 +208,12 @@ const RelatedDOIs: React.FC = (props) => { - + From 0fa5ed4978d2ba52596ca85281dc8ea35c07aa5c Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 2 Nov 2023 14:34:07 +0000 Subject: [PATCH 63/68] #1531 - fix some tests --- .../cypress/e2e/DOIGenerationForm.cy.ts | 12 +++++++++--- packages/datagateway-download/src/testData.ts | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts index 20e076800..f418e9d4d 100644 --- a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -64,7 +64,9 @@ describe('DOI Generation form', () => { cy.contains('button', 'Generate DOI').click(); cy.contains('Mint Confirmation').should('be.visible'); - cy.contains('Mint was successful').should('be.visible'); + cy.contains('Mint was successful', { timeout: 10000 }).should( + 'be.visible' + ); cy.contains('View Data Publication').click(); cy.url().should('match', /\/browse\/dataPublication\/[0-9]+$/); @@ -121,7 +123,9 @@ describe('DOI Generation form', () => { // check that contributor info doesn't break the API cy.contains('button', 'Generate DOI').click(); - cy.contains('Mint was successful').should('be.visible'); + cy.contains('Mint was successful', { timeout: 10000 }).should( + 'be.visible' + ); }); it('should not let user add invalid/duplicate Data Publication users', () => { @@ -192,7 +196,9 @@ describe('DOI Generation form', () => { // check that related DOIs info doesn't break the API cy.contains('button', 'Generate DOI').click(); - cy.contains('Mint was successful').should('be.visible'); + cy.contains('Mint was successful', { timeout: 10000 }).should( + 'be.visible' + ); }); it('should let user see their current cart items', () => { diff --git a/packages/datagateway-download/src/testData.ts b/packages/datagateway-download/src/testData.ts index 6957bae93..f28c2593f 100644 --- a/packages/datagateway-download/src/testData.ts +++ b/packages/datagateway-download/src/testData.ts @@ -239,6 +239,7 @@ export const mockedSettings: Partial = { downloadApiUrl: 'https://example.com/downloadApi', idsUrl: 'https://example.com/ids', doiMinterUrl: 'https://example.com/doiMinter', + dataCiteUrl: 'https://example.com/dataCite', fileCountMax: 5000, totalSizeMax: 1000000000000, accessMethods: { From 7d6e862a9b842c8388283df9abf10419ffef305d Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Thu, 2 Nov 2023 14:51:05 +0000 Subject: [PATCH 64/68] Increase Jest timeouts --- packages/datagateway-common/src/setupTests.tsx | 2 +- packages/datagateway-dataview/src/setupTests.ts | 2 +- packages/datagateway-download/src/setupTests.ts | 2 +- packages/datagateway-search/src/setupTests.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/datagateway-common/src/setupTests.tsx b/packages/datagateway-common/src/setupTests.tsx index 8a92f8061..27978c8c0 100644 --- a/packages/datagateway-common/src/setupTests.tsx +++ b/packages/datagateway-common/src/setupTests.tsx @@ -13,7 +13,7 @@ import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import { createMemoryHistory, History } from 'history'; -jest.setTimeout(15000); +jest.setTimeout(20000); if (typeof window.URL.createObjectURL === 'undefined') { // required as work-around for enzyme/jest environment not implementing window.URL.createObjectURL method diff --git a/packages/datagateway-dataview/src/setupTests.ts b/packages/datagateway-dataview/src/setupTests.ts index ef0657630..314b188ae 100644 --- a/packages/datagateway-dataview/src/setupTests.ts +++ b/packages/datagateway-dataview/src/setupTests.ts @@ -9,7 +9,7 @@ import { initialState as dgDataViewInitialState } from './state/reducers/dgdatav import { dGCommonInitialState } from 'datagateway-common'; import { screen, within } from '@testing-library/react'; -jest.setTimeout(15000); +jest.setTimeout(20000); function noOp(): void { // required as work-around for enzyme/jest environment not implementing window.URL.createObjectURL method diff --git a/packages/datagateway-download/src/setupTests.ts b/packages/datagateway-download/src/setupTests.ts index 47066dac6..379d1619c 100644 --- a/packages/datagateway-download/src/setupTests.ts +++ b/packages/datagateway-download/src/setupTests.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import '@testing-library/jest-dom'; -jest.setTimeout(15000); +jest.setTimeout(20000); function noOp(): void { // required as work-around for enzyme/jest environment not implementing window.URL.createObjectURL method diff --git a/packages/datagateway-search/src/setupTests.ts b/packages/datagateway-search/src/setupTests.ts index f39931b30..690ce993c 100644 --- a/packages/datagateway-search/src/setupTests.ts +++ b/packages/datagateway-search/src/setupTests.ts @@ -8,7 +8,7 @@ import { dGCommonInitialState } from 'datagateway-common'; import { initialState as dgSearchInitialState } from './state/reducers/dgsearch.reducer'; import { screen, within } from '@testing-library/react'; -jest.setTimeout(15000); +jest.setTimeout(20000); // Unofficial React 17 Enzyme adapter Enzyme.configure({ adapter: new Adapter() }); From 959e853c4f13c9fe7e50cdcef94e2fc9cda3f352 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 17 Jan 2024 10:54:38 +0000 Subject: [PATCH 65/68] Remove .only from test & fix settings context declaration --- .../cypress/e2e/card/dls/datasets.cy.ts | 2 +- .../downloadCart/downloadCartTable.component.tsx | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts index 30e74020d..5337a08d3 100644 --- a/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/card/dls/datasets.cy.ts @@ -109,7 +109,7 @@ describe('DLS - Datasets Cards', () => { cy.get('[data-testid="card"]').first().contains('DATASET 61'); }); - it.only('should be able to filter by multiple fields', () => { + it('should be able to filter by multiple fields', () => { cy.get('[data-testid="advanced-filters-link"]').click(); cy.get('[aria-label="Filter by Name"]').first().type('61'); cy.wait(['@getDatasetsCount', '@getDatasetsOrder'], { timeout: 10000 }); diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index c1b58f3c2..fb0b20162 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -52,9 +52,14 @@ interface DownloadCartTableProps { const DownloadCartTable: React.FC = ( props: DownloadCartTableProps ) => { - const { fileCountMax, totalSizeMax, apiUrl, facilityName } = React.useContext( - DownloadSettingsContext - ); + const { + fileCountMax, + totalSizeMax, + apiUrl, + facilityName, + doiMinterUrl, + dataCiteUrl, + } = React.useContext(DownloadSettingsContext); const [sort, setSort] = React.useState<{ [column: string]: Order }>({}); const [filters, setFilters] = React.useState<{ @@ -591,7 +596,7 @@ const DownloadCartTable: React.FC = ( {t('downloadCart.remove_all')} - {settings.doiMinterUrl && settings.dataCiteUrl && ( + {doiMinterUrl && dataCiteUrl && ( Date: Wed, 17 Jan 2024 16:22:35 +0000 Subject: [PATCH 66/68] #1529 #1531 - fix how we send entity IDs to minter DOI --- .../datagateway-download/src/downloadApi.ts | 12 +++++------ .../src/downloadApiHooks.test.tsx | 20 +++++++++---------- .../src/downloadApiHooks.ts | 5 ++++- .../downloadCartTable.component.tsx | 3 ++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 6cec9dd0e..d6cdb9b69 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -413,10 +413,10 @@ export const isCartMintable = async ( `${doiMinterUrl}/ismintable`, { ...(investigations.length > 0 - ? { investigations: { ids: investigations } } + ? { investigation_ids: investigations } : {}), - ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), - ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), + ...(datasets.length > 0 ? { dataset_ids: datasets } : {}), + ...(datafiles.length > 0 ? { datafile_ids: datafiles } : {}), }, { headers: { @@ -572,10 +572,10 @@ export const mintCart = ( resource_type: investigations.length === 0 ? 'Dataset' : 'Collection', }, ...(investigations.length > 0 - ? { investigations: { ids: investigations } } + ? { investigation_ids: investigations } : {}), - ...(datasets.length > 0 ? { datasets: { ids: datasets } } : {}), - ...(datafiles.length > 0 ? { datafiles: { ids: datafiles } } : {}), + ...(datasets.length > 0 ? { dataset_ids: datasets } : {}), + ...(datafiles.length > 0 ? { datafile_ids: datafiles } : {}), }, { headers: { diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx index bcf601987..80802955b 100644 --- a/packages/datagateway-download/src/downloadApiHooks.test.tsx +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -1391,9 +1391,9 @@ describe('Download API react-query hooks test', () => { expect(axios.post).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/ismintable`, { - investigations: { ids: [1, 2] }, - datasets: { ids: [3] }, - datafiles: { ids: [4] }, + investigation_ids: [1, 2], + dataset_ids: [3], + datafile_ids: [4], }, { headers: { Authorization: 'Bearer null' } } ); @@ -1462,7 +1462,7 @@ describe('Download API react-query hooks test', () => { expect(axios.post).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/ismintable`, { - investigations: { ids: [1] }, + investigation_ids: [1], }, { headers: { Authorization: 'Bearer null' } } ); @@ -1502,7 +1502,7 @@ describe('Download API react-query hooks test', () => { expect(axios.post).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/ismintable`, { - datafiles: { ids: [4] }, + datafile_ids: [4], }, { headers: { Authorization: 'Bearer null' } } ); @@ -1573,9 +1573,9 @@ describe('Download API react-query hooks test', () => { ...doiMetadata, resource_type: 'Collection', }, - investigations: { ids: [1, 2] }, - datasets: { ids: [3] }, - datafiles: { ids: [4] }, + investigation_ids: [1, 2], + dataset_ids: [3], + datafile_ids: [4], }, { headers: { Authorization: 'Bearer null' } } ); @@ -1611,7 +1611,7 @@ describe('Download API react-query hooks test', () => { ...doiMetadata, resource_type: 'Collection', }, - investigations: { ids: [1] }, + investigation_ids: [1], }, { headers: { Authorization: 'Bearer null' } } ); @@ -1655,7 +1655,7 @@ describe('Download API react-query hooks test', () => { ...doiMetadata, resource_type: 'Dataset', }, - datafiles: { ids: [4] }, + datafile_ids: [4], }, { headers: { Authorization: 'Bearer null' } } ); diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 53001c8f8..75014d1a0 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -795,7 +795,10 @@ export const useDownloadPercentageComplete = ({ */ export const useIsCartMintable = ( cart: DownloadCartItem[] | undefined -): UseQueryResult> => { +): UseQueryResult< + boolean, + AxiosError<{ detail: { msg: string }[] } | { detail: string }> +> => { const settings = React.useContext(DownloadSettingsContext); const { doiMinterUrl } = settings; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index fb0b20162..0a9596031 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -182,7 +182,8 @@ const DownloadCartTable: React.FC = ( const unmintableEntityIDs: number[] | null | undefined = React.useMemo( () => - mintableError?.response?.data.detail && + mintableError?.response?.status === 403 && + typeof mintableError?.response?.data?.detail === 'string' && JSON.parse( mintableError.response.data.detail.substring( mintableError.response.data.detail.indexOf('['), From 93796b0df1ad227a212a2ae74d47c83b04858b4c Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 17 Jan 2024 16:52:46 +0000 Subject: [PATCH 67/68] #1531 fix test error by updating api error message when an invalid user is entered --- .../datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts index f418e9d4d..ae2d605e1 100644 --- a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -159,7 +159,9 @@ describe('DOI Generation form', () => { 'have.length', 1 ); - cy.contains('No record found: invalid in User').should('be.visible'); + cy.contains("No record found: name='Invalid' in User").should( + 'be.visible' + ); cy.contains('button', 'Generate DOI').should('not.be.disabled'); }); From ae1fb812efd93ecbb9b0e67447c9744bf9218eb1 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Wed, 17 Jan 2024 16:53:15 +0000 Subject: [PATCH 68/68] Fix casing --- .../datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts index ae2d605e1..94b3f544e 100644 --- a/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts +++ b/packages/datagateway-download/cypress/e2e/DOIGenerationForm.cy.ts @@ -159,7 +159,7 @@ describe('DOI Generation form', () => { 'have.length', 1 ); - cy.contains("No record found: name='Invalid' in User").should( + cy.contains("No record found: name='invalid' in User").should( 'be.visible' );