From 5b4c581a6df485651e0fc61093483b2aa845c94c Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 11 Jan 2024 10:28:36 -0500 Subject: [PATCH 1/5] CRDCDH-730 Disable disallowed validation targets Also migrates usage of GenericAlert to notistack for this comp. --- package-lock.json | 31 ++++++++++ package.json | 1 + src/App.tsx | 22 +++++-- .../DataSubmissions/ValidationControls.tsx | 59 ++++++++++++------- src/components/GenericAlert/index.tsx | 5 ++ .../StyledNotistackAlerts/index.tsx | 29 +++++++++ src/types/Submissions.d.ts | 8 ++- 7 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 src/components/StyledNotistackAlerts/index.tsx diff --git a/package-lock.json b/package-lock.json index affcf341..b086bb48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "dayjs": "^1.11.8", "graphql": "^16.7.1", "lodash": "^4.17.21", + "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", @@ -9649,6 +9650,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "license": "MIT", @@ -13148,6 +13157,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "license": "MIT", @@ -17387,6 +17417,7 @@ }, "node_modules/typescript": { "version": "5.1.3", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 6ee125cb..5122f232 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dayjs": "^1.11.8", "graphql": "^16.7.1", "lodash": "^4.17.21", + "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", diff --git a/src/App.tsx b/src/App.tsx index ce11b943..1ca51e19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { SnackbarProvider } from 'notistack'; import { ThemeProvider, CssBaseline, createTheme } from "@mui/material"; import { LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import routeConfig from "./router"; +import StyledNotistackAlerts from './components/StyledNotistackAlerts'; declare module '@mui/material/styles' { interface PaletteOptions { @@ -42,10 +44,22 @@ const router = createBrowserRouter(routeConfig); function App() { return ( - - - - + + + + + + ); } diff --git a/src/components/DataSubmissions/ValidationControls.tsx b/src/components/DataSubmissions/ValidationControls.tsx index 0006955e..76086329 100644 --- a/src/components/DataSubmissions/ValidationControls.tsx +++ b/src/components/DataSubmissions/ValidationControls.tsx @@ -2,10 +2,10 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { useMutation } from '@apollo/client'; import { FormControlLabel, RadioGroup, styled } from '@mui/material'; import { LoadingButton } from '@mui/lab'; +import { useSnackbar } from 'notistack'; import { useAuthContext } from '../Contexts/AuthContext'; import StyledRadioButton from "../Questionnaire/StyledRadioButton"; import { VALIDATE_SUBMISSION, ValidateSubmissionResp } from '../../graphql'; -import GenericAlert, { AlertState } from '../GenericAlert'; type Props = { /** @@ -137,15 +137,35 @@ const ValidateStatuses: Submission["status"][] = ["In Progress", "Withdrawn", "R */ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => { const { user } = useAuthContext(); - const [validationType, setValidationType] = useState("Metadata"); - const [uploadType, setUploadType] = useState("New"); + const { enqueueSnackbar } = useSnackbar(); + + const [validationType, setValidationType] = useState(null); + const [uploadType, setUploadType] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isValidating, setIsValidating] = useState(dataSubmission?.fileValidationStatus === "Validating" || dataSubmission?.metadataValidationStatus === "Validating"); - const [validationAlert, setValidationAlert] = useState(null); - const canValidateData: boolean = useMemo(() => ValidateRoles.includes(user?.role), [user?.role]); - const validateButtonEnabled: boolean = useMemo(() => ValidateStatuses.includes(dataSubmission?.status), [dataSubmission?.status]); + const canValidateMetadata: boolean = useMemo(() => { + if (!user?.role || ValidateRoles.includes(user?.role) === false) { + return false; + } + if (!dataSubmission?.status || ValidateStatuses.includes(dataSubmission?.status) === false) { + return false; + } + + return dataSubmission?.metadataValidationStatus !== null; + }, [user?.role, dataSubmission?.metadataValidationStatus]); + + const canValidateFiles: boolean = useMemo(() => { + if (!user?.role || ValidateRoles.includes(user?.role) === false) { + return false; + } + if (!dataSubmission?.status || ValidateStatuses.includes(dataSubmission?.status) === false) { + return false; + } + + return dataSubmission?.fileValidationStatus !== null; + }, [user?.role, dataSubmission?.fileValidationStatus]); const [validateSubmission] = useMutation(VALIDATE_SUBMISSION, { context: { clientName: 'backend' }, @@ -168,20 +188,18 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => }); if (errors || !data?.validateSubmission?.success) { - setValidationAlert({ message: "Unable to initiate validation process.", severity: "error" }); + enqueueSnackbar("Unable to initiate validation process.", { variant: "error" }); setIsValidating(false); onValidate?.(false); } else { - setValidationAlert({ message: "Validation process is starting; this may take some time. Please wait before initiating another validation.", severity: "success" }); + enqueueSnackbar("Validation process is starting; this may take some time. Please wait before initiating another validation.", { variant: "success" }); setIsValidating(true); onValidate?.(true); } - // Reset form to default values - setValidationType("Metadata"); - setUploadType("New"); + setValidationType(null); + setUploadType(null); setIsLoading(false); - setTimeout(() => setValidationAlert(null), 10000); }; const getTypes = (validationType: ValidationType): string[] => { @@ -202,31 +220,28 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => return ( - - {validationAlert?.message} -
Validation Type:
- setValidationType(val)} row> + setValidationType(val)} row> } label="Validate Metadata" - disabled={!canValidateData} + disabled={!canValidateMetadata} /> } label="Validate Data Files" - disabled={!canValidateData} + disabled={!canValidateFiles} /> } label="Both" - disabled={!canValidateData} + disabled={!canValidateFiles || !canValidateMetadata} />
@@ -239,13 +254,13 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => value="New" control={} label="New Uploaded Data" - disabled={!canValidateData} + disabled={!canValidateFiles && !canValidateMetadata} /> } label="All Uploaded Data" - disabled={!canValidateData} + disabled={!canValidateFiles && !canValidateMetadata} />
@@ -254,7 +269,7 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => diff --git a/src/components/GenericAlert/index.tsx b/src/components/GenericAlert/index.tsx index 4b4cad87..916aeb4b 100644 --- a/src/components/GenericAlert/index.tsx +++ b/src/components/GenericAlert/index.tsx @@ -31,6 +31,11 @@ type Props = { children: React.ReactNode; }; +/** + * Basic alert component that can be used to display a message to the user. + * + * @deprecated DO NOT USE. Replaced by `enqueueSnackbar` from Notistack. + */ const GenericAlert : FC = ({ open, children, diff --git a/src/components/StyledNotistackAlerts/index.tsx b/src/components/StyledNotistackAlerts/index.tsx new file mode 100644 index 00000000..ab14c5d9 --- /dev/null +++ b/src/components/StyledNotistackAlerts/index.tsx @@ -0,0 +1,29 @@ +import { styled } from '@mui/material'; +import { MaterialDesignContent } from 'notistack'; + +const BaseSnackbarStyles = { + color: '#ffffff', + width: '535px', + minHeight: '50px', + boxShadow: '-4px 8px 27px 4px rgba(27,28,28,0.09)', + boxSizing: 'border-box', + userSelect: 'none', + justifyContent: 'center', +}; + +const StyledNotistackAlerts = styled(MaterialDesignContent)({ + '&.notistack-MuiContent-default': { + ...BaseSnackbarStyles, + backgroundColor: '#5D53F6', + }, + '&.notistack-MuiContent-error': { + ...BaseSnackbarStyles, + backgroundColor: '#E74040', + }, + '&.notistack-MuiContent-success': { + ...BaseSnackbarStyles, + backgroundColor: '#5D53F6', + }, +}); + +export default StyledNotistackAlerts; diff --git a/src/types/Submissions.d.ts b/src/types/Submissions.d.ts index 32f4ce14..af492ecb 100644 --- a/src/types/Submissions.d.ts +++ b/src/types/Submissions.d.ts @@ -20,7 +20,13 @@ type Submission = { updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z }; -type ValidationStatus = "New" | "Validating" | "Passed" | "Error" | "Warning"; +/** + * The status of a Metadata or Files in a submission. + * + * @note `null` indicates that the type has not been uploaded yet. + * @note `New` indicates that the type has been uploaded but not validated yet. + */ +type ValidationStatus = null | "New" | "Validating" | "Passed" | "Error" | "Warning"; type SubmissionStatus = | "New" From 2ac3148f400e4c0a4847e47aa60b06352700dd76 Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 11 Jan 2024 11:05:27 -0500 Subject: [PATCH 2/5] fix: Validate button still enabled when Validating is ongoing --- .../DataSubmissions/ValidationControls.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/DataSubmissions/ValidationControls.tsx b/src/components/DataSubmissions/ValidationControls.tsx index 76086329..9718f092 100644 --- a/src/components/DataSubmissions/ValidationControls.tsx +++ b/src/components/DataSubmissions/ValidationControls.tsx @@ -129,6 +129,17 @@ const StyledRadioControl = styled(FormControlLabel)({ const ValidateRoles: User["role"][] = ["Submitter", "Data Curator", "Organization Owner", "Admin"]; const ValidateStatuses: Submission["status"][] = ["In Progress", "Withdrawn", "Rejected"]; +const getTypes = (validationType: ValidationType): string[] => { + switch (validationType) { + case "Metadata": + return ["metadata"]; + case "Files": + return ["file"]; + default: + return ["metadata", "file"]; + } +}; + /** * Provides the UI for validating a data submission's assets. * @@ -202,17 +213,6 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => setIsLoading(false); }; - const getTypes = (validationType: ValidationType): string[] => { - switch (validationType) { - case "Metadata": - return ["metadata"]; - case "Files": - return ["file"]; - default: - return ["metadata", "file"]; - } - }; - useEffect(() => { setIsValidating(dataSubmission?.fileValidationStatus === "Validating" || dataSubmission?.metadataValidationStatus === "Validating"); @@ -269,7 +269,7 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => From e1e183a8fcb09d203c1e2c80a420fc531068183d Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 11 Jan 2024 11:35:24 -0500 Subject: [PATCH 3/5] fix: Icon should not be visible in Notistack alerts --- src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/App.tsx b/src/App.tsx index 1ca51e19..fae84fff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,6 +53,7 @@ function App() { error: StyledNotistackAlerts, success: StyledNotistackAlerts, }} + hideIconVariant preventDuplicate > From 8c02dd566e7c5cbc69407f95741709edbf3327f9 Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 11 Jan 2024 13:32:42 -0500 Subject: [PATCH 4/5] fix: Validating without selection is allowed --- src/components/DataSubmissions/ValidationControls.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/DataSubmissions/ValidationControls.tsx b/src/components/DataSubmissions/ValidationControls.tsx index 9718f092..3cd08d1b 100644 --- a/src/components/DataSubmissions/ValidationControls.tsx +++ b/src/components/DataSubmissions/ValidationControls.tsx @@ -184,7 +184,13 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => }); const handleValidateFiles = async () => { - if (isValidating) { + if (isValidating || !validationType || !uploadType) { + return; + } + if (!canValidateFiles && validationType === "Files") { + return; + } + if (!canValidateMetadata && validationType === "Metadata") { return; } From d745eecfbd6b2b52e90d55cab2f76c86bde80e9c Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 11 Jan 2024 14:54:33 -0500 Subject: [PATCH 5/5] CRDCDH-730 Refactor default validation selection --- .../DataSubmissions/ValidationControls.tsx | 37 +++++++++---------- src/types/Submissions.d.ts | 10 +++++ src/utils/dataValidationUtils.ts | 35 ++++++++++++++++++ src/utils/index.ts | 1 + 4 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 src/utils/dataValidationUtils.ts diff --git a/src/components/DataSubmissions/ValidationControls.tsx b/src/components/DataSubmissions/ValidationControls.tsx index 3cd08d1b..de6019a4 100644 --- a/src/components/DataSubmissions/ValidationControls.tsx +++ b/src/components/DataSubmissions/ValidationControls.tsx @@ -6,6 +6,7 @@ import { useSnackbar } from 'notistack'; import { useAuthContext } from '../Contexts/AuthContext'; import StyledRadioButton from "../Questionnaire/StyledRadioButton"; import { VALIDATE_SUBMISSION, ValidateSubmissionResp } from '../../graphql'; +import { getDefaultValidationType, getValidationTypes } from '../../utils'; type Props = { /** @@ -22,10 +23,6 @@ type Props = { onValidate: (success: boolean) => void; }; -type ValidationType = "Metadata" | "Files" | "All"; - -type UploadType = "New" | "All"; - const StyledValidateButton = styled(LoadingButton)({ alignSelf: "center", display: "flex", @@ -129,17 +126,6 @@ const StyledRadioControl = styled(FormControlLabel)({ const ValidateRoles: User["role"][] = ["Submitter", "Data Curator", "Organization Owner", "Admin"]; const ValidateStatuses: Submission["status"][] = ["In Progress", "Withdrawn", "Rejected"]; -const getTypes = (validationType: ValidationType): string[] => { - switch (validationType) { - case "Metadata": - return ["metadata"]; - case "Files": - return ["file"]; - default: - return ["metadata", "file"]; - } -}; - /** * Provides the UI for validating a data submission's assets. * @@ -151,7 +137,7 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => const { enqueueSnackbar } = useSnackbar(); const [validationType, setValidationType] = useState(null); - const [uploadType, setUploadType] = useState(null); + const [uploadType, setUploadType] = useState("New"); const [isLoading, setIsLoading] = useState(false); const [isValidating, setIsValidating] = useState(dataSubmission?.fileValidationStatus === "Validating" || dataSubmission?.metadataValidationStatus === "Validating"); @@ -199,7 +185,7 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => const { data, errors } = await validateSubmission({ variables: { _id: dataSubmission?._id, - types: getTypes(validationType), + types: getValidationTypes(validationType), scope: uploadType === "New" ? "New" : "All", } }); @@ -214,8 +200,8 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => onValidate?.(true); } - setValidationType(null); - setUploadType(null); + setValidationType(getDefaultValidationType(dataSubmission)); + setUploadType("New"); setIsLoading(false); }; @@ -224,6 +210,17 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) => || dataSubmission?.metadataValidationStatus === "Validating"); }, [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]); + useEffect(() => { + if (validationType !== null) { + return; + } + if (typeof dataSubmission === "undefined") { + return; + } + + setValidationType(getDefaultValidationType(dataSubmission)); + }, [dataSubmission]); + return (
@@ -255,7 +252,7 @@ const ValidationControls: FC = ({ dataSubmission, onValidate }: Props) =>
Validation Target:
- setUploadType(val)} row> + setUploadType(val)} row> } diff --git a/src/types/Submissions.d.ts b/src/types/Submissions.d.ts index af492ecb..7b7382c3 100644 --- a/src/types/Submissions.d.ts +++ b/src/types/Submissions.d.ts @@ -223,3 +223,13 @@ type DataValidationResult = { */ message: string; }; + +/** + * The type of Data Validation to perform. + */ +type ValidationType = "Metadata" | "Files" | "All"; + +/** + * The target of Data Validation action. + */ +type ValidationTarget = "New" | "All"; diff --git a/src/utils/dataValidationUtils.ts b/src/utils/dataValidationUtils.ts new file mode 100644 index 00000000..6181de94 --- /dev/null +++ b/src/utils/dataValidationUtils.ts @@ -0,0 +1,35 @@ +/** + * Translates the Validation Type radio to an array of types to validate. + * + * @param validationType The validation type selected. + * @returns The array of types to validate. + */ +export const getValidationTypes = (validationType: ValidationType): string[] => { + switch (validationType) { + case "Metadata": + return ["metadata"]; + case "Files": + return ["file"]; + default: + return ["metadata", "file"]; + } +}; + +/** + * Determines the default "Validation Type" for the given data submission. + * + * @param dataSubmission The data submission to get the default validation type for. + * @returns The default validation type for the given data submission. + */ +export const getDefaultValidationType = (dataSubmission: Submission): ValidationType => { + const { metadataValidationStatus, fileValidationStatus } = dataSubmission || {}; + + if (metadataValidationStatus !== null) { + return "Metadata"; + } + if (fileValidationStatus !== null) { + return "Files"; + } + + return "Metadata"; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 3d8af6a3..3eb37f8b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,3 +6,4 @@ export * from './formModeUtils'; export * from './profileUtils'; export * from './historyUtils'; export * from './dataModelUtils'; +export * from './dataValidationUtils';