From 163a29c8906334deed0743a7c726180ab5566994 Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 8 Feb 2021 08:47:51 +0200 Subject: [PATCH 01/10] Add submissionType to submitted object --- .../NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js | 2 ++ .../NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index d685d062..f176f2ab 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -361,6 +361,7 @@ const WizardFillObjectDetailsForm = () => { const classes = useStyles() const objectType = useSelector(state => state.objectType) + const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(false) const [errorPrefix, setErrorPrefix] = useState("") @@ -391,6 +392,7 @@ const WizardFillObjectDetailsForm = () => { addObjectToFolder(folderId, { accessionId: response.data.accessionId, schema: objectType, + tags: { submissionType: "Form" }, }) ) .then(() => { diff --git a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js index 7b0ce9f6..f2751aab 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js @@ -93,6 +93,7 @@ const WizardUploadObjectXMLForm = () => { addObjectToFolder(folderId, { accessionId: response.data.accessionId, schema: objectType, + tags: { submissionType: "XML" }, }) ) } else { From f3c3afa1b4d32230e4f3ea8b64043df7cc07d448 Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 8 Feb 2021 09:37:21 +0200 Subject: [PATCH 02/10] Add submissionType to submitted items list --- .../WizardSavedObjectsList.js | 64 +++++++++++-------- .../WizardSteps/WizardAddObjectStep.js | 7 +- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index d3706bdf..8931c17b 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -60,18 +60,25 @@ const ToggleMessage = ({ delay, children }: { delay: number, children: any }) => /** * List objects by submission type. Enables deletion of objects */ -const WizardSavedObjectsList = ({ submissionType, submissions }: { submissionType: string, submissions: any }) => { +const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { const ref = useRef() useEffect(() => { ref.current = submissions }) + const classes = useStyles() + const dispatch = useDispatch() const objectType = useSelector(state => state.objectType) const [connError, setConnError] = useState(false) const [responseError, setResponseError] = useState({}) const [errorPrefix, setErrorPrefix] = useState("") const newObject = submissions.filter(x => !ref.current?.includes(x)) + // filter submissionTypes that exist in current submissions & sort them according to alphabetical order + const submissionTypes = submissions + .map(obj => obj.tags.submissionType) + .filter((val, ind, arr) => arr.indexOf(val) === ind) + .sort() const handleObjectDelete = objectId => { setConnError(false) @@ -84,30 +91,37 @@ const WizardSavedObjectsList = ({ submissionType, submissions }: { submissionTyp return (
-

Submitted {submissionType} items

- - {submissions.map(submission => { - return ( - - - - {newObject.length === 1 && newObject[0]?.accessionId === submission.accessionId && ( - Added! - )} - { - handleObjectDelete(submission.accessionId) - }} - edge="end" - aria-label="delete" - > - - - - - ) - })} - + {submissionTypes.map(submissionType => ( + <> +

+ Submitted {objectType} {submissionType} +

+ + {submissions.map( + submission => + submission.tags.submissionType === submissionType && ( + + + + {newObject.length === 1 && newObject[0]?.accessionId === submission.accessionId && ( + Added! + )} + { + handleObjectDelete(submission.accessionId) + }} + edge="end" + aria-label="delete" + > + + + + + ) + )} + + + ))} {connError && ( )} diff --git a/src/components/NewDraftWizard/WizardSteps/WizardAddObjectStep.js b/src/components/NewDraftWizard/WizardSteps/WizardAddObjectStep.js index 2708c421..bba4cb45 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardAddObjectStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardAddObjectStep.js @@ -35,9 +35,8 @@ const useStyles = makeStyles(theme => ({ const WizardAddObjectStep = () => { const classes = useStyles() const objectType = useSelector(state => state.objectType) - const currentSubmissionType = useSelector(state => state.objectType) const folder = useSelector(state => state.submissionFolder) - const submissions = folder?.metadataObjects?.filter(obj => obj.schema === currentSubmissionType) + const submissions = folder?.metadataObjects?.filter(obj => obj.schema === objectType) return ( <> @@ -55,9 +54,7 @@ const WizardAddObjectStep = () => { )}
- {submissions?.length > 0 && ( - - )} + {submissions?.length > 0 && } ) From eaf32fee91098aa56382a3d77dc12708710114bc Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 8 Feb 2021 12:29:49 +0200 Subject: [PATCH 03/10] Add unit test for submitted items list --- src/__tests__/WizardSavedObjectsList.test.js | 40 +++++++++++--- .../WizardSavedObjectsList.js | 52 +++++++++---------- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/__tests__/WizardSavedObjectsList.test.js b/src/__tests__/WizardSavedObjectsList.test.js index 39fd2d34..08efe895 100644 --- a/src/__tests__/WizardSavedObjectsList.test.js +++ b/src/__tests__/WizardSavedObjectsList.test.js @@ -1,7 +1,7 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" -import { render, screen } from "@testing-library/react" +import { render, screen, within } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" @@ -11,23 +11,51 @@ const mockStore = configureStore([]) describe("WizardStepper", () => { const store = mockStore({ - submissionType: "sample", + objectType: "sample", wizardStep: 1, }) const submissions = [ - { accessionId: "EDAG1", schema: "sample" }, - { accessionId: "EDAG2", schema: "sample" }, + { accessionId: "EDAG1", schema: "sample", tags: { submissionType: "Form" } }, + { accessionId: "EDAG2", schema: "sample", tags: { submissionType: "XML" } }, + { accessionId: "EDAG3", schema: "sample", tags: { submissionType: "XML" } }, ] - it("should have saved objects listed", () => { + beforeEach(() => { render( - + ) + }) + + it("should have saved objects listed", () => { submissions.forEach(item => { expect(screen.getByText(item.accessionId)).toBeInTheDocument() }) }) + + it("should have correct amount of submitted forms", () => { + screen.getByText(/Submitted sample form/i) + expect(screen.getByText(/Submitted sample form/i)).toBeInTheDocument() + + const formList = screen.getByRole("list", { name: "Form" }) + expect(formList).toBeInTheDocument() + + const { getAllByRole } = within(formList) + const submittedForms = getAllByRole("listitem") + expect(submittedForms.length).toBe(1) + }) + + it("should have correct amount of submitted forms", () => { + screen.getByText(/Submitted sample xml/i) + expect(screen.getByText(/Submitted sample xml/i)).toBeInTheDocument() + + const xmlList = screen.getByRole("list", { name: "XML" }) + expect(xmlList).toBeInTheDocument() + + const { getAllByRole } = within(xmlList) + const submittedXML = getAllByRole("listitem") + expect(submittedXML.length).toBe(2) + }) }) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index 8931c17b..b56fe26f 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -92,35 +92,33 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { return (
{submissionTypes.map(submissionType => ( - <> +

- Submitted {objectType} {submissionType} + Submitted {`${objectType.charAt(0).toUpperCase()}${objectType.slice(1)}`} {submissionType}

- - {submissions.map( - submission => - submission.tags.submissionType === submissionType && ( - - - - {newObject.length === 1 && newObject[0]?.accessionId === submission.accessionId && ( - Added! - )} - { - handleObjectDelete(submission.accessionId) - }} - edge="end" - aria-label="delete" - > - - - - - ) - )} - - + {submissions.map( + submission => + submission.tags.submissionType === submissionType && ( + + + + {newObject.length === 1 && newObject[0]?.accessionId === submission.accessionId && ( + Added! + )} + { + handleObjectDelete(submission.accessionId) + }} + edge="end" + aria-label="delete" + > + + + + + ) + )} +
))} {connError && ( From fd28f6b53a794ba3f5fe3c404e4de63df833dcdc Mon Sep 17 00:00:00 2001 From: HangLe Date: Mon, 8 Feb 2021 15:45:51 +0200 Subject: [PATCH 04/10] Fix for title of submitted list --- .../WizardSavedObjectsList.js | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index b56fe26f..80e9828e 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -80,6 +80,12 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { .filter((val, ind, arr) => arr.indexOf(val) === ind) .sort() + // group submissions according to their submissionType + const groupedSubmissions = submissionTypes.map(submissionType => ({ + submissionType, + submittedItems: submissions.filter(obj => obj.tags.submissionType === submissionType), + })) + const handleObjectDelete = objectId => { setConnError(false) dispatch(deleteObjectFromFolder("submitted", objectId, objectType)).catch(error => { @@ -89,35 +95,47 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { }) } + const displayObjectType = (objectType: string) => { + return `${objectType.charAt(0).toUpperCase()}${objectType.slice(1)}` + } + + const displaySubmissionType = (submission: { submissionType: string, submittedItems: any }) => { + switch (submission.submissionType) { + case "Form": + return submission.submittedItems.length >= 2 ? "Forms" : "Form" + case "XML": + return submission.submittedItems.length >= 2 ? "XML files" : "XML file" + default: + break + } + } + return (
- {submissionTypes.map(submissionType => ( - + {groupedSubmissions.map(group => ( +

- Submitted {`${objectType.charAt(0).toUpperCase()}${objectType.slice(1)}`} {submissionType} + Submitted {displayObjectType(objectType)} {displaySubmissionType(group)}

- {submissions.map( - submission => - submission.tags.submissionType === submissionType && ( - - - - {newObject.length === 1 && newObject[0]?.accessionId === submission.accessionId && ( - Added! - )} - { - handleObjectDelete(submission.accessionId) - }} - edge="end" - aria-label="delete" - > - - - - - ) - )} + {group.submittedItems.map(item => ( + + + + {newObject.length === 1 && newObject[0]?.accessionId === item.accessionId && ( + Added! + )} + { + handleObjectDelete(item.accessionId) + }} + edge="end" + aria-label="delete" + > + + + + + ))}
))} {connError && ( From efbc844f86bfa7a92e48b769a89769c9ae3a008c Mon Sep 17 00:00:00 2001 From: Sauli Purhonen Date: Thu, 11 Feb 2021 08:16:26 +0200 Subject: [PATCH 05/10] Edit saved form & replace saved XML functionality --- cypress/integration/app.spec.js | 12 ++ .../WizardComponents/WizardAlert.js | 157 +++++++++++++----- .../WizardDraftObjectPicker.js | 4 +- .../WizardComponents/WizardObjectIndex.js | 5 +- .../WizardSavedObjectsList.js | 110 +++++++----- .../WizardFillObjectDetailsForm.js | 138 ++++++++++----- .../WizardForms/WizardStatusMessageHandler.js | 36 +++- .../WizardForms/WizardUploadObjectXMLForm.js | 78 ++++++--- src/features/wizardCurrentObjectSlice.js | 16 ++ src/features/wizardDraftObjectSlice.js | 16 -- src/features/wizardSubmissionFolderSlice.js | 22 +++ src/rootReducer.js | 4 +- src/services/objectAPI.js | 12 ++ 13 files changed, 438 insertions(+), 172 deletions(-) create mode 100644 src/features/wizardCurrentObjectSlice.js delete mode 100644 src/features/wizardDraftObjectSlice.js diff --git a/cypress/integration/app.spec.js b/cypress/integration/app.spec.js index 679bde57..818a5954 100644 --- a/cypress/integration/app.spec.js +++ b/cypress/integration/app.spec.js @@ -35,6 +35,18 @@ describe("Basic e2e", function () { cy.get("button[type=submit]").contains("Submit").click() cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) + // Edit saved submission + cy.get("button[type=button]").contains("New form").click() + cy.get("button[type=button]").contains("Edit").click() + cy.get("input[name='descriptor.studyTitle']").should("have.value", "New title") + cy.get("input[name='descriptor.studyTitle']").type(" edited") + cy.get("input[name='descriptor.studyTitle']").should("have.value", "New title edited") + cy.get("button[type=button]").contains("Update").click() + cy.get("div[role=alert]").contains("Object updated") + cy.get("button[type=button]").contains("New form").click() + cy.get("button[type=button]").contains("Edit").click() + cy.get("input[name='descriptor.studyTitle']").should("have.value", "New title edited") + // Upload a Study xml file. cy.get("div[role=button]").contains("Upload XML File").click() cy.fixture("study_test.xml").then(fileContent => { diff --git a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js index 767f6a59..9f1ba5ea 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js @@ -12,14 +12,15 @@ import { useDispatch, useSelector } from "react-redux" import { resetDraftStatus } from "features/draftStatusSlice" import { setAlert, resetAlert } from "features/wizardAlertSlice" -import { resetDraftObject } from "features/wizardDraftObjectSlice" +import { resetCurrentObject } from "features/wizardCurrentObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" import { addObjectToDrafts } from "features/wizardSubmissionFolderSlice" import draftAPIService from "services/draftAPI" +import objectAPIService from "services/objectAPI" // Simple template for error messages const ErrorMessage = message => { - return {message} + return {message.message} } /* @@ -37,7 +38,7 @@ const CancelFormDialog = ({ currentSubmissionType: string, }) => { const submissionFolder = useSelector(state => state.submissionFolder) - const draftObject = useSelector(state => state.draftObject) + const currentObject = useSelector(state => state.currentObject) const objectType = useSelector(state => state.objectType) const [error, setError] = useState(false) const [errorMessage, setErrorMessage] = useState("") @@ -47,8 +48,13 @@ const CancelFormDialog = ({ const saveDraft = async () => { setError(false) const err = "Connection error, cannot save draft." - if (draftObject.draftId) { - const response = await draftAPIService.patchFromJSON(objectType, draftObject.draftId, draftObject) + + if ((currentObject.accessionId || currentObject.objectId) && currentObject.type === "draft") { + const response = await draftAPIService.patchFromJSON( + objectType, + currentObject.accessionId || currentObject.objectId, + currentObject.cleanedValues + ) if (response.ok) { dispatch(resetDraftStatus()) dispatch( @@ -58,14 +64,14 @@ const CancelFormDialog = ({ errorPrefix: "", }) ) - dispatch(resetDraftObject()) + dispatch(resetCurrentObject()) handleDialog(true) } else { setError(true) setErrorMessage(err) } } else { - const response = await draftAPIService.createFromJSON(objectType, draftObject) + const response = await draftAPIService.createFromJSON(objectType, currentObject) if (response.ok) { dispatch( updateStatus({ @@ -81,7 +87,7 @@ const CancelFormDialog = ({ schema: "draft-" + objectType, }) ) - dispatch(resetDraftObject()) + dispatch(resetCurrentObject()) handleDialog(true) } else { setError(true) @@ -90,53 +96,112 @@ const CancelFormDialog = ({ } } + const updateForm = async () => { + const err = "Connection error, cannot update object" + const response = await objectAPIService.patchFromJSON( + objectType, + currentObject.accessionId, + currentObject.cleanedValues + ) + if (response.ok) { + dispatch(resetDraftStatus()) + dispatch( + updateStatus({ + successStatus: "success", + response: response, + errorPrefix: "", + }) + ) + dispatch(resetCurrentObject()) + handleDialog(true) + } else { + setError(true) + setErrorMessage(err) + } + } + let [dialogTitle, dialogContent] = ["", ""] let dialogActions const formContent = "If you save form as a draft, you can continue filling it later." const xmlContent = "If you save xml as a draft, you can upload it later." const objectContent = "If you save object as a draft, you can upload it later." + switch (parentLocation) { case "submission": { - switch (alertType) { - case "form": { - dialogTitle = "Would you like to save draft version of this form" - dialogContent = formContent - break - } - case "xml": { - dialogTitle = "Would you like to save draft version of this xml upload" - dialogContent = xmlContent - break - } - case "existing": { - dialogTitle = "Would you like to save draft version of this existing object upload" - dialogContent = objectContent - break - } - default: { - dialogTitle = "default" - dialogContent = "default content" + if (currentObject?.type === "saved") { + dialogTitle = "Would you like to save edited form data?" + dialogContent = "Unsaved changes will be lost. If you save form as a draft, you can continue filling it later." + dialogActions = ( + + + + + + + ) + } else { + switch (alertType) { + case "form": { + dialogTitle = "Would you like to save draft version of this form" + dialogContent = formContent + break + } + case "xml": { + dialogTitle = "Would you like to save draft version of this xml upload" + dialogContent = xmlContent + break + } + case "existing": { + dialogTitle = "Would you like to save draft version of this existing object upload" + dialogContent = objectContent + break + } + default: { + dialogTitle = "default" + dialogContent = "default content" + } } + dialogActions = ( + + + + + + ) } - dialogActions = ( - - - - - - ) + break } case "footer": { diff --git a/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js index 9ece8a80..46555eae 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js @@ -16,7 +16,7 @@ import { useSelector, useDispatch } from "react-redux" import WizardStatusMessageHandler from "../WizardForms/WizardStatusMessageHandler" import { resetFocus } from "features/focusSlice" -import { setDraftObject } from "features/wizardDraftObjectSlice" +import { setCurrentObject } from "features/wizardCurrentObjectSlice" import { deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" import { setSubmissionType } from "features/wizardSubmissionTypeSlice" import draftAPIService from "services/draftAPI" @@ -78,7 +78,7 @@ const WizardDraftObjectPicker = () => { setConnError(false) const response = await draftAPIService.getObjectByAccessionId(objectType, objectId) if (response.ok) { - dispatch(setDraftObject(response.data)) + dispatch(setCurrentObject({ ...response.data, type: "draft" })) dispatch(setSubmissionType("form")) } else { setConnError(true) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js index d844250c..f287f8cf 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js @@ -17,7 +17,7 @@ import WizardAlert from "./WizardAlert" import { resetDraftStatus } from "features/draftStatusSlice" import { setFocus } from "features/focusSlice" -import { resetDraftObject } from "features/wizardDraftObjectSlice" +import { resetCurrentObject } from "features/wizardCurrentObjectSlice" import { setObjectType } from "features/wizardObjectTypeSlice" import { setSubmissionType } from "features/wizardSubmissionTypeSlice" @@ -247,7 +247,7 @@ const WizardObjectIndex = () => { setClickedSubmissionType(submissionType) setCancelFormOpen(true) } else { - dispatch(resetDraftObject()) + dispatch(resetCurrentObject()) dispatch(resetDraftStatus()) dispatch(setSubmissionType(submissionType)) dispatch(setObjectType(expandedObjectType)) @@ -262,6 +262,7 @@ const WizardObjectIndex = () => { dispatch(resetDraftStatus()) dispatch(setSubmissionType(clickedSubmissionType)) dispatch(setObjectType(expandedObjectType)) + dispatch(resetCurrentObject()) } } diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index 80e9828e..740e3c11 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -1,18 +1,23 @@ //@flow import React, { useEffect, useState, useRef } from "react" -import IconButton from "@material-ui/core/IconButton" +import Button from "@material-ui/core/Button" +import ButtonGroup from "@material-ui/core/ButtonGroup" +// import IconButton from "@material-ui/core/IconButton" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" import ListItemText from "@material-ui/core/ListItemText" import { makeStyles } from "@material-ui/core/styles" -import ClearIcon from "@material-ui/icons/Clear" +// import ClearIcon from "@material-ui/icons/Clear" import { useSelector, useDispatch } from "react-redux" import WizardStatusMessageHandler from "../WizardForms/WizardStatusMessageHandler" +import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" import { deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" +import { setSubmissionType } from "features/wizardSubmissionTypeSlice" +import objectAPIService from "services/objectAPI" const useStyles = makeStyles(theme => ({ objectList: { @@ -30,33 +35,23 @@ const useStyles = makeStyles(theme => ({ alignItems: "flex-start", padding: ".5rem", }, - addedMessage: { - color: theme.palette.success.main, - visibility: "visible", - opacity: "1", - transition: "opacity 2s linear", + listItemText: { + display: "inline-block", + maxWidth: "50%", + "& span": { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, }, - hidden: { - color: theme.palette.success.main, - visibility: "hidden", - opacity: "0", - transition: "visibility 0s .2s, opacity .2s linear", + buttonEdit: { + color: "#007bff", + }, + buttonDelete: { + color: "#dc3545", }, })) -const ToggleMessage = ({ delay, children }: { delay: number, children: any }) => { - const classes = useStyles() - const [visible, setVisible] = useState(true) - useEffect(() => { - const toggle = setTimeout(() => { - setVisible(false) - }, delay) - return () => clearTimeout(toggle) - }, [delay]) - - return {children} -} - /** * List objects by submission type. Enables deletion of objects */ @@ -73,7 +68,9 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { const [connError, setConnError] = useState(false) const [responseError, setResponseError] = useState({}) const [errorPrefix, setErrorPrefix] = useState("") - const newObject = submissions.filter(x => !ref.current?.includes(x)) + + const currentObject = useSelector(state => state.currentObject) + // filter submissionTypes that exist in current submissions & sort them according to alphabetical order const submissionTypes = submissions .map(obj => obj.tags.submissionType) @@ -86,13 +83,42 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { submittedItems: submissions.filter(obj => obj.tags.submissionType === submissionType), })) - const handleObjectDelete = objectId => { + const handleObjectEdit = async (objectId, submissionType, tags) => { + setConnError(false) + const response = await objectAPIService.getObjectByAccessionId(objectType, objectId) + if (response.ok) { + dispatch( + setCurrentObject({ + ...response.data, + type: "saved", + tags: tags, + index: submissions.findIndex(item => item.accessionId === objectId), + }) + ) + dispatch(setSubmissionType(submissionType.toLowerCase())) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Object fetching error") + } + } + + const handleObjectDelete = (objectId, submissionType) => { setConnError(false) dispatch(deleteObjectFromFolder("submitted", objectId, objectType)).catch(error => { setConnError(true) setResponseError(JSON.parse(error)) setErrorPrefix("Can't delete object") }) + + if ( + submissions.filter(item => item.tags.submissionType === submissionType).length - 1 === 555 && + currentObject.tags.submissionType === submissionType.toLowerCase() + ) { + dispatch(resetCurrentObject()) + } + + if (currentObject.accessionId === objectId) dispatch(resetCurrentObject()) } const displayObjectType = (objectType: string) => { @@ -119,20 +145,24 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { {group.submittedItems.map(item => ( - + - {newObject.length === 1 && newObject[0]?.accessionId === item.accessionId && ( - Added! - )} - { - handleObjectDelete(item.accessionId) - }} - edge="end" - aria-label="delete" - > - - + + + + ))} diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index f176f2ab..c809b48a 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -10,6 +10,7 @@ import { makeStyles } from "@material-ui/core/styles" import AddCircleOutlinedIcon from "@material-ui/icons/AddCircleOutlined" import Alert from "@material-ui/lab/Alert" import Ajv from "ajv" +import { cloneDeep } from "lodash" import { useForm, FormProvider } from "react-hook-form" import { useDispatch, useSelector } from "react-redux" @@ -19,7 +20,7 @@ import WizardStatusMessageHandler from "./WizardStatusMessageHandler" import { setDraftStatus, resetDraftStatus } from "features/draftStatusSlice" import { resetFocus } from "features/focusSlice" -import { setDraftObject, resetDraftObject } from "features/wizardDraftObjectSlice" +import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" import { addObjectToFolder, addObjectToDrafts, deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" import draftAPIService from "services/draftAPI" @@ -90,6 +91,7 @@ const useStyles = makeStyles(theme => ({ type CustomCardHeaderProps = { objectType: string, + currentObject: any, title: string, onClickNewForm: () => void, onClickClearForm: () => void, @@ -104,6 +106,7 @@ type FormContentProps = { onSubmit: () => Promise, objectType: string, folderId: string, + currentObject: any, } /* @@ -111,7 +114,16 @@ type FormContentProps = { */ const CustomCardHeader = (props: CustomCardHeaderProps) => { const classes = useStyles() - const { objectType, title, refForm, onClickNewForm, onClickClearForm, onClickSaveDraft, onClickSubmit } = props + const { + objectType, + currentObject, + title, + refForm, + onClickNewForm, + onClickClearForm, + onClickSaveDraft, + onClickSubmit, + } = props const dispatch = useDispatch() @@ -150,11 +162,11 @@ const CustomCardHeader = (props: CustomCardHeaderProps) => { variant="contained" aria-label="submit form" size="small" - type="submit" + type={currentObject?.type === "saved" ? "button" : "submit"} onClick={onClickSubmit} form={refForm} > - Submit {objectType} + {currentObject?.type === "saved" ? "Update" : "Submit"} {objectType}
) @@ -175,28 +187,39 @@ const CustomCardHeader = (props: CustomCardHeaderProps) => { /* * Return react-hook-form based form which is rendered from schema and checked against resolver. Set default values when continuing draft */ -const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: FormContentProps) => { +const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId, currentObject }: FormContentProps) => { const classes = useStyles() + const dispatch = useDispatch() const draftStatus = useSelector(state => state.draftStatus) - const draftObject = useSelector(state => state.draftObject) const alert = useSelector(state => state.alert) - const methods = useForm({ mode: "onBlur", resolver, defaultValues: draftObject }) + const methods = useForm({ mode: "onBlur", resolver }) - const dispatch = useDispatch() const [cleanedValues, setCleanedValues] = useState({}) - const [currentDraftId, setCurrentDraftId] = useState(draftObject?.accessionId) + const [currentObjectId, setCurrentObjectId] = useState(currentObject?.accessionId) + const [timer, setTimer] = useState(0) const increment = useRef(null) + // Set form default values + useEffect(() => { + methods.reset(currentObject) + setCleanedValues(currentObject) + }, [currentObject?.accessionId]) + + // Check if form has been edited + useEffect(() => { + checkDirty() + }, [methods.formState.isDirty]) + const createNewForm = () => { handleReset() resetForm() - setCurrentDraftId(null) + setCurrentObjectId(null) dispatch(resetDraftStatus()) - dispatch(resetDraftObject()) + dispatch(resetCurrentObject()) } const resetForm = () => { @@ -209,20 +232,33 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F } } - useEffect(() => { - checkDirty() - }, [methods.formState.isDirty]) - + // Draft data is set to state on every change to form const handleChange = () => { + const clone = cloneDeep(currentObject) const values = JSONSchemaParser.cleanUpFormValues(methods.getValues()) setCleanedValues(values) - dispatch(setDraftObject(Object.assign(values, { draftId: currentDraftId }))) + + // Original values have protected keys + Object.keys(values).forEach(item => (clone[item] = values[item])) + + !currentObject.accessionId && currentObjectId + ? dispatch( + setCurrentObject({ + ...clone, + cleanedValues: values, + type: currentObject.type || "draft", + objectId: currentObjectId, + }) + ) + : dispatch(setCurrentObject({ ...clone, cleanedValues: values })) + checkDirty() } const handleDraftDelete = draftId => { dispatch(deleteObjectFromFolder("draft", draftId, objectType)) - setCurrentDraftId(null) + setCurrentObjectId(() => null) + handleChange() } /* @@ -252,32 +288,40 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F } }, []) + // Form actions + const patchHandler = response => { + if (response.ok) { + dispatch(resetDraftStatus()) + dispatch( + updateStatus({ + successStatus: "success", + response: response, + errorPrefix: "", + }) + ) + } else { + dispatch( + updateStatus({ + successStatus: "error", + response: response, + errorPrefix: "Unexpected error", + }) + ) + } + } + + /* + * Update or save new draft depending on object status + */ const saveDraft = async () => { handleReset() - if (currentDraftId || draftObject.accessionId) { - const response = await draftAPIService.patchFromJSON(objectType, currentDraftId, cleanedValues) - if (response.ok) { - dispatch(resetDraftStatus()) - dispatch( - updateStatus({ - successStatus: "success", - response: response, - errorPrefix: "", - }) - ) - } else { - dispatch( - updateStatus({ - successStatus: "error", - response: response, - errorPrefix: "Unexpected error", - }) - ) - } + if ((currentObjectId || currentObject?.accessionId) && currentObject?.type === "draft") { + const response = await draftAPIService.patchFromJSON(objectType, currentObjectId, cleanedValues) + patchHandler(response) } else { const response = await draftAPIService.createFromJSON(objectType, cleanedValues) if (response.ok) { - setCurrentDraftId(response.data.accessionId) + if (currentObject?.type !== "saved") setCurrentObjectId(response.data.accessionId) dispatch(resetDraftStatus()) dispatch( addObjectToDrafts(folderId, { @@ -315,6 +359,13 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F } } + const patchObject = async () => { + handleReset() + const response = await objectAPIService.patchFromJSON(objectType, currentObjectId, cleanedValues) + patchHandler(response) + } + + // Clear auto-save timer useEffect(() => { if (alert) { clearInterval(increment.current) @@ -326,15 +377,18 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F }, [timer]) const submitForm = () => { + if (currentObject?.type === "saved") patchObject() + if (currentObject?.type === "draft" && currentObjectId && Object.keys(currentObject).length > 0) + handleDraftDelete(currentObjectId) + handleReset() - dispatch(resetDraftObject()) - if (currentDraftId && methods.formState.isValid) handleDraftDelete(currentDraftId) } return ( createNewForm()} @@ -372,6 +426,7 @@ const WizardFillObjectDetailsForm = () => { const dispatch = useDispatch() const { id: folderId } = useSelector(state => state.submissionFolder) const [responseInfo, setResponseInfo] = useState([]) + const currentObject = useSelector(state => state.currentObject) /* * Submit form with cleaned values and check for response errors @@ -398,6 +453,7 @@ const WizardFillObjectDetailsForm = () => { .then(() => { setSuccessStatus("success") dispatch(resetDraftStatus()) + dispatch(resetCurrentObject()) }) .catch(error => { setSuccessStatus("error") @@ -452,6 +508,8 @@ const WizardFillObjectDetailsForm = () => { onSubmit={onSubmit} objectType={objectType} folderId={folderId} + currentObject={currentObject} + key={currentObject?.accessionId || folderId} /> {submitting && } {successStatus && ( diff --git a/src/components/NewDraftWizard/WizardForms/WizardStatusMessageHandler.js b/src/components/NewDraftWizard/WizardForms/WizardStatusMessageHandler.js index ef70e82b..175a8831 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardStatusMessageHandler.js +++ b/src/components/NewDraftWizard/WizardForms/WizardStatusMessageHandler.js @@ -54,10 +54,38 @@ const InfoHandler = ({ handleClose }: { handleClose: boolean => void }) => { // Success messages const SuccessHandler = ({ response, handleClose }: { response: any, handleClose: boolean => void }) => { - const message = - response.config.baseURL === "/drafts" - ? `Draft saved with accessionid ${response.data.accessionId}` - : `Submitted with accessionid ${response.data.accessionId}` + let message = "" + + switch (response.config.baseURL) { + case "/drafts": { + switch (response.config.method) { + case "patch": { + message = `Draft updated with accessionid ${response.data.accessionId}` + break + } + default: { + message = `Draft saved with accessionid ${response.data.accessionId}` + } + } + break + } + case "/objects": { + switch (response.config.method) { + case "patch": { + message = `Object updated with accessionid ${response.data.accessionId}` + break + } + case "put": { + message = `Object replaced with accessionid ${response.data.accessionId}` + break + } + default: { + message = `Submitted with accessionid ${response.data.accessionId}` + } + } + } + } + return ( handleClose(false)} severity="success"> {message} diff --git a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js index f2751aab..66ef0664 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js @@ -15,7 +15,8 @@ import { useDispatch, useSelector } from "react-redux" import WizardStatusMessageHandler from "./WizardStatusMessageHandler" import { resetFocus } from "features/focusSlice" -import { addObjectToFolder } from "features/wizardSubmissionFolderSlice" +import { resetCurrentObject } from "features/wizardCurrentObjectSlice" +import { addObjectToFolder, replaceObjectInFolder } from "features/wizardSubmissionFolderSlice" import objectAPIService from "services/objectAPI" import submissionAPIService from "services/submissionAPI" @@ -65,8 +66,9 @@ const WizardUploadObjectXMLForm = () => { const { id: folderId } = useSelector(state => state.submissionFolder) const dispatch = useDispatch() const classes = useStyles() - - const { register, errors, watch, handleSubmit } = useForm({ mode: "onChange" }) + const currentObject = useSelector(state => state.currentObject) + const { register, errors, watch, handleSubmit, reset } = useForm({ mode: "onChange" }) + const [placeHolder, setPlaceHolder] = useState("Name") const watchFile = watch("fileUpload") @@ -77,6 +79,21 @@ const WizardUploadObjectXMLForm = () => { if (shouldFocus && focusTarget.current) focusTarget.current.focus() }, [shouldFocus]) + useEffect(() => { + if (watchFile && watchFile[0] && watchFile[0].name) { + setPlaceHolder(watchFile[0].name) + } else { + setPlaceHolder(currentObject?.tags?.fileName || "Name") + } + }, [currentObject, watchFile]) + + const resetForm = () => { + reset() + setPlaceHolder("Name") + } + + const fileName = watchFile && watchFile[0] ? watchFile[0].name : "No file name" + const onSubmit = async data => { setSuccessStatus(undefined) setSubmitting(true) @@ -85,19 +102,43 @@ const WizardUploadObjectXMLForm = () => { setSuccessStatus("info") }, 5000) - const response = await objectAPIService.createFromXML(objectType, file) - setResponseStatus(response) - if (response.ok) { - setSuccessStatus("success") - dispatch( - addObjectToFolder(folderId, { - accessionId: response.data.accessionId, - schema: objectType, - tags: { submissionType: "XML" }, - }) - ) + if (currentObject.accessionId) { + const response = await objectAPIService.replaceXML(objectType, currentObject.accessionId, file) + setResponseStatus(response) + if (response.ok) { + dispatch( + replaceObjectInFolder(folderId, currentObject.accessionId, currentObject.index, { + submissionType: "XML", + fileName: fileName, + }) + ) + .then(() => { + setSuccessStatus("success") + resetForm() + dispatch(resetCurrentObject()) + }) + .catch(() => { + setSuccessStatus("error") + }) + } else { + setSuccessStatus("error") + } } else { - setSuccessStatus("error") + const response = await objectAPIService.createFromXML(objectType, file) + setResponseStatus(response) + if (response.ok) { + setSuccessStatus("success") + dispatch( + addObjectToFolder(folderId, { + accessionId: response.data.accessionId, + schema: objectType, + tags: { submissionType: "XML", fileName }, + }) + ) + resetForm() + } else { + setSuccessStatus("error") + } } clearTimeout(waitForServertimer) setSubmitting(false) @@ -118,7 +159,7 @@ const WizardUploadObjectXMLForm = () => { disabled={isSubmitting || !watchFile || watchFile.length === 0 || errors.fileUpload != null} onClick={handleSubmit(onSubmit)} > - Submit + {currentObject?.type === "saved" ? "Replace" : "Submit"} ) @@ -137,10 +178,7 @@ const WizardUploadObjectXMLForm = () => {
- + + + + ) +} + +export default WizardSavedObjectActions diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index 740e3c11..21d3c040 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -1,23 +1,14 @@ //@flow -import React, { useEffect, useState, useRef } from "react" +import React, { useEffect, useRef } from "react" -import Button from "@material-ui/core/Button" -import ButtonGroup from "@material-ui/core/ButtonGroup" -// import IconButton from "@material-ui/core/IconButton" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" import ListItemText from "@material-ui/core/ListItemText" import { makeStyles } from "@material-ui/core/styles" -// import ClearIcon from "@material-ui/icons/Clear" -import { useSelector, useDispatch } from "react-redux" +import { useSelector } from "react-redux" -import WizardStatusMessageHandler from "../WizardForms/WizardStatusMessageHandler" - -import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" -import { deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" -import { setSubmissionType } from "features/wizardSubmissionTypeSlice" -import objectAPIService from "services/objectAPI" +import WizardSavedObjectActions from "./WizardSavedObjectActions" const useStyles = makeStyles(theme => ({ objectList: { @@ -62,14 +53,7 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { }) const classes = useStyles() - - const dispatch = useDispatch() const objectType = useSelector(state => state.objectType) - const [connError, setConnError] = useState(false) - const [responseError, setResponseError] = useState({}) - const [errorPrefix, setErrorPrefix] = useState("") - - const currentObject = useSelector(state => state.currentObject) // filter submissionTypes that exist in current submissions & sort them according to alphabetical order const submissionTypes = submissions @@ -83,44 +67,6 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { submittedItems: submissions.filter(obj => obj.tags.submissionType === submissionType), })) - const handleObjectEdit = async (objectId, submissionType, tags) => { - setConnError(false) - const response = await objectAPIService.getObjectByAccessionId(objectType, objectId) - if (response.ok) { - dispatch( - setCurrentObject({ - ...response.data, - type: "saved", - tags: tags, - index: submissions.findIndex(item => item.accessionId === objectId), - }) - ) - dispatch(setSubmissionType(submissionType.toLowerCase())) - } else { - setConnError(true) - setResponseError(response) - setErrorPrefix("Object fetching error") - } - } - - const handleObjectDelete = (objectId, submissionType) => { - setConnError(false) - dispatch(deleteObjectFromFolder("submitted", objectId, objectType)).catch(error => { - setConnError(true) - setResponseError(JSON.parse(error)) - setErrorPrefix("Can't delete object") - }) - - if ( - submissions.filter(item => item.tags.submissionType === submissionType).length - 1 === 555 && - currentObject.tags.submissionType === submissionType.toLowerCase() - ) { - dispatch(resetCurrentObject()) - } - - if (currentObject.accessionId === objectId) dispatch(resetCurrentObject()) - } - const displayObjectType = (objectType: string) => { return `${objectType.charAt(0).toUpperCase()}${objectType.slice(1)}` } @@ -147,30 +93,18 @@ const WizardSavedObjectsList = ({ submissions }: { submissions: any }) => { - - - - + ))} ))} - {connError && ( - - )}
) } diff --git a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js index 1ebc2ea2..b6246cef 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardShowSummaryStep.js @@ -1,13 +1,16 @@ //@flow import React from "react" +import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" +import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" import ListItemText from "@material-ui/core/ListItemText" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import { useSelector } from "react-redux" import WizardHeader from "../WizardComponents/WizardHeader" +import WizardSavedObjectActions from "../WizardComponents/WizardSavedObjectActions" import WizardStepper from "../WizardComponents/WizardStepper" const useStyles = makeStyles(theme => ({ @@ -28,6 +31,9 @@ const useStyles = makeStyles(theme => ({ fontWeight: "bold", }, }, + listGroup: { + padding: "0", + }, objectListItems: { border: "solid 1px #ccc", borderRadius: 3, @@ -39,7 +45,7 @@ const useStyles = makeStyles(theme => ({ type Schema = "Study" | "Sample" | "Experiment" | "Run" | "Analysis" | "DAC" | "Policy" -type GroupedBySchema = {| [Schema]: string[] |} +type GroupedBySchema = {| [Schema]: Object[] |} /** * Show summary of objects added to folder @@ -55,11 +61,9 @@ const WizardShowSummaryStep = () => { "Analysis", "DAC", "Policy", - ].map((schema: string) => { + ].map((schema: Object) => { return { - [(schema: string)]: metadataObjects - .filter(object => object.schema.toLowerCase() === schema.toLowerCase()) - .map(object => object.accessionId), + [(schema: Object)]: metadataObjects.filter(object => object.schema.toLowerCase() === schema.toLowerCase()), } }) const classes = useStyles() @@ -72,7 +76,7 @@ const WizardShowSummaryStep = () => { {groupedObjects.map(group => { const schema = Object.keys(group)[0] return ( -
+
{schema} @@ -80,13 +84,23 @@ const WizardShowSummaryStep = () => {
{group[schema].length}
- {group[schema].map(accessionId => ( - - + {group[schema].map(item => ( + + + + + ))}
-
+ ) })}
From b573ea8cdd3b80af0b64f68ff7f5d93c0d284b03 Mon Sep 17 00:00:00 2001 From: Sauli Purhonen Date: Fri, 12 Feb 2021 10:54:21 +0200 Subject: [PATCH 10/10] Test snackbar alert when replacing XML file --- cypress/integration/app.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/integration/app.spec.js b/cypress/integration/app.spec.js index e7660264..a39e98af 100644 --- a/cypress/integration/app.spec.js +++ b/cypress/integration/app.spec.js @@ -76,6 +76,7 @@ describe("Basic e2e", function () { }) cy.get("form").submit() cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 2) + cy.contains(".MuiAlert-message", "Object replaced") // Fill an Analysis form and submit object cy.get("div[role=button]").contains("Analysis").click()