diff --git a/cypress/integration/draft.spec.js b/cypress/integration/draft.spec.js index 9466948d..8bd930b7 100644 --- a/cypress/integration/draft.spec.js +++ b/cypress/integration/draft.spec.js @@ -35,6 +35,7 @@ describe("Draft operations", function () { cy.get("div[role=button]").contains("Fill Form").click() cy.get("h2").contains("Would you like to save draft version of this form") cy.get("div[role=dialog]").contains("Save").click() + cy.wait(500) cy.get("ul[data-testid='Draft-objects']").find("li").should("have.length", 2) // Delete a draft diff --git a/cypress/integration/home.spec.js b/cypress/integration/home.spec.js index 4f0fcdd8..d0619e61 100644 --- a/cypress/integration/home.spec.js +++ b/cypress/integration/home.spec.js @@ -37,6 +37,12 @@ describe("Home e2e", function () { cy.get("button[type=button]").contains("Save as Draft").click() cy.get("div[role=alert]", { timeout: 10000 }).contains("Draft saved with") + // Fill a Sample form draft + cy.clickFillForm("Sample") + cy.get("input[data-testid='title']").type("Test sample") + cy.get("button[type=button]").contains("Save as Draft").click() + cy.get("div[role=alert]", { timeout: 10000 }).contains("Draft saved with") + // Save folder and navigate to Home page cy.get("button[type=button]").contains("Save and Exit").click() cy.get('button[aria-label="Save a new folder and move to frontpage"]').contains("Return to homepage").click() @@ -72,7 +78,18 @@ describe("Home e2e", function () { // Navigate to home & edit submitted object cy.findDraftFolder("Edited unpublished folder") - cy.get('button[aria-label="Edit this object"]').click() + + // Edit draft Sample form + cy.get("tr[data-testid='Test sample']").within(() => cy.get('button[aria-label="Edit this object"]').click()) + cy.get("input[data-testid='title']").type(" edited") + cy.get("button[type=button]").contains("Update draft").click() + cy.get("div[role=alert]", { timeout: 10000 }).contains("Draft updated with") + + // Navigate to home & check updated draft's new title + cy.findDraftFolder("Edited unpublished folder") + cy.get("tr[data-testid='Test sample edited']").should("be.visible") + + cy.get("tr[data-testid='Second test title']").within(() => cy.get('button[aria-label="Edit this object"]').click()) cy.get("input[name='descriptor.studyTitle']").type(" edited") cy.get("button[type=button]").contains("Update").click() cy.get("div[role=alert]").contains("Object updated") diff --git a/src/components/Home/SelectedFolderDetails.js b/src/components/Home/SelectedFolderDetails.js index 7dfa2583..cb0b2670 100644 --- a/src/components/Home/SelectedFolderDetails.js +++ b/src/components/Home/SelectedFolderDetails.js @@ -65,7 +65,6 @@ const SelectedFolderDetails = (): React$Element => { const handleObject = async (data: any, draft: boolean, objectType: string, objectInFolder: any) => { const service = draft ? draftAPIService : objectAPIService - const response = await service.getObjectByAccessionId(objectType, objectInFolder.accessionId) if (response.ok) { diff --git a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js index d0a7b994..aced2de7 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js @@ -57,7 +57,7 @@ const CancelFormDialog = ({ currentObject.accessionId || currentObject.objectId, objectType, currentObject.status, - submissionFolder.folderId, + submissionFolder, currentObject.cleanedValues || currentObject, dispatch ) diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index 79d6f987..8c3de458 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -27,9 +27,10 @@ import { setDraftStatus, resetDraftStatus } from "features/draftStatusSlice" import { resetFocus } from "features/focusSlice" import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" -import { deleteObjectFromFolder, modifyObjectTags } from "features/wizardSubmissionFolderSlice" +import { deleteObjectFromFolder, replaceObjectInFolder } from "features/wizardSubmissionFolderSlice" import objectAPIService from "services/objectAPI" import schemaAPIService from "services/schemaAPI" +import type { FolderDetailsWithId } from "types" import { getObjectDisplayTitle, formatDisplayObjectType } from "utils" import { dereferenceSchema } from "utils/JSONSchemaUtils" @@ -120,7 +121,7 @@ type FormContentProps = { formSchema: any, onSubmit: () => Promise, objectType: string, - folderId: string, + folder: FolderDetailsWithId, currentObject: any, } @@ -194,7 +195,7 @@ 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, currentObject }: FormContentProps) => { +const FormContent = ({ resolver, formSchema, onSubmit, objectType, folder, currentObject }: FormContentProps) => { const classes = useStyles() const dispatch = useDispatch() @@ -331,14 +332,18 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId, cur */ const patchHandler = (response, cleanedValues) => { if (response.ok) { + const index = folder.metadataObjects.findIndex(item => item.accessionId === currentObject.accessionId) dispatch( - modifyObjectTags({ - accessionId: currentObject.accessionId, - tags: { + replaceObjectInFolder( + folder.folderId, + currentObject.accessionId, + index, + { submissionType: ObjectSubmissionTypes.form, displayTitle: getObjectDisplayTitle(objectType, cleanedValues), }, - }) + ObjectStatus.submitted + ) ) dispatch(resetDraftStatus()) dispatch( @@ -371,7 +376,7 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId, cur currentObject.accessionId || currentObject.objectId, objectType, currentObject.status, - folderId, + folder, cleanedValues, dispatch ) @@ -435,7 +440,7 @@ const WizardFillObjectDetailsForm = (): React$Element => { const dispatch = useDispatch() const objectType = useSelector(state => state.objectType) - const { folderId, metadataObjects } = useSelector(state => state.submissionFolder) + const folder = useSelector(state => state.submissionFolder) const currentObject = useSelector(state => state.currentObject) // States that will update in useEffect() @@ -488,7 +493,7 @@ const WizardFillObjectDetailsForm = (): React$Element => { }, [objectType]) const getAccessionIds = (objectType: string) => { - const submissions = metadataObjects?.filter(obj => obj.schema.toLowerCase() === objectType) + const submissions = folder.metadataObjects?.filter(obj => obj.schema.toLowerCase() === objectType) // Add "- Title: " to accessionId, special case DAC form: add "- Main Contact:" const accessionIds = submissions?.map(obj => obj.schema === ObjectTypes.dac @@ -680,7 +685,7 @@ const WizardFillObjectDetailsForm = (): React$Element => { */ const onSubmit = async data => { setSubmitting(true) - const response = await submitObjectHook(data, folderId, objectType, dispatch) + const response = await submitObjectHook(data, folder.folderId, objectType, dispatch) if (response) setSubmitting(false) } @@ -696,9 +701,9 @@ const WizardFillObjectDetailsForm = (): React$Element => { resolver={WizardAjvResolver(states.validationSchema)} onSubmit={onSubmit} objectType={objectType} - folderId={folderId} + folder={folder} currentObject={currentObject} - key={currentObject?.accessionId || folderId} + key={currentObject?.accessionId || folder.folderId} /> {submitting && } diff --git a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js index 53eba268..42b023c8 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js @@ -115,10 +115,16 @@ const WizardUploadObjectXMLForm = (): React$Element => { setResponseStatus(response) if (response.ok) { dispatch( - replaceObjectInFolder(folderId, currentObject.accessionId, currentObject.index, { - submissionType: ObjectSubmissionTypes.xml, - fileName: fileName, - }) + replaceObjectInFolder( + folderId, + currentObject.accessionId, + currentObject.index, + { + submissionType: ObjectSubmissionTypes.xml, + fileName: fileName, + }, + ObjectStatus.submitted + ) ) .then(() => { setSuccessStatus(WizardStatus.success) diff --git a/src/components/NewDraftWizard/WizardHooks/WizardSaveDraftHook.js b/src/components/NewDraftWizard/WizardHooks/WizardSaveDraftHook.js index a48fe59b..b91c40ff 100644 --- a/src/components/NewDraftWizard/WizardHooks/WizardSaveDraftHook.js +++ b/src/components/NewDraftWizard/WizardHooks/WizardSaveDraftHook.js @@ -1,19 +1,21 @@ //@flow + import { ObjectStatus } from "constants/wizardObject" import { WizardStatus } from "constants/wizardStatus" import { resetDraftStatus } from "features/draftStatusSlice" import { setLoading, resetLoading } from "features/loadingSlice" import { resetCurrentObject } from "features/wizardCurrentObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" -import { addObjectToDrafts, modifyDraftObjectTags } from "features/wizardSubmissionFolderSlice" +import { addObjectToDrafts, replaceObjectInFolder } from "features/wizardSubmissionFolderSlice" import draftAPIService from "services/draftAPI" +import type { FolderDetailsWithId } from "types" import { getObjectDisplayTitle } from "utils" const saveDraftHook = async ( accessionId?: string, objectType: string, objectStatus: string, - folderId: string, + folder: FolderDetailsWithId, values: any, dispatch: function ): any => { @@ -21,15 +23,17 @@ const saveDraftHook = async ( dispatch(setLoading()) if (accessionId && objectStatus === ObjectStatus.draft) { const response = await draftAPIService.patchFromJSON(objectType, accessionId, values) + const index = folder.drafts.findIndex(item => item.accessionId === accessionId) if (response.ok) { dispatch(resetDraftStatus()) dispatch( - modifyDraftObjectTags({ + replaceObjectInFolder( + folder.folderId, accessionId, - tags: { - displayTitle: draftDisplayTitle, - }, - }) + index, + { displayTitle: draftDisplayTitle }, + ObjectStatus.draft + ) ) dispatch( updateStatus({ @@ -62,7 +66,7 @@ const saveDraftHook = async ( ) dispatch(resetDraftStatus()) dispatch( - addObjectToDrafts(folderId, { + addObjectToDrafts(folder.folderId, { accessionId: response.data.accessionId, schema: "draft-" + objectType, tags: { displayTitle: draftDisplayTitle }, diff --git a/src/features/wizardSubmissionFolderSlice.js b/src/features/wizardSubmissionFolderSlice.js index 7f89d20f..3f9acdfb 100644 --- a/src/features/wizardSubmissionFolderSlice.js +++ b/src/features/wizardSubmissionFolderSlice.js @@ -56,155 +56,171 @@ export const { } = wizardSubmissionFolderSlice.actions export default wizardSubmissionFolderSlice.reducer -export const createNewDraftFolder = ( - folderDetails: FolderDataFromForm -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const folderForBackend: FolderDetails = { - ...folderDetails, - published: false, - metadataObjects: [], - drafts: [], +export const createNewDraftFolder = + (folderDetails: FolderDataFromForm): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const folderForBackend: FolderDetails = { + ...folderDetails, + published: false, + metadataObjects: [], + drafts: [], + } + const response = await folderAPIService.createNewFolder(folderForBackend) + + return new Promise((resolve, reject) => { + if (response.ok) { + const folder: FolderDetailsWithId = { + ...folderForBackend, + folderId: response.data.folderId, + } + dispatch(setFolder(folder)) + resolve(response) + } else { + reject(JSON.stringify(response)) + } + }) } - const response = await folderAPIService.createNewFolder(folderForBackend) - return new Promise((resolve, reject) => { - if (response.ok) { - const folder: FolderDetailsWithId = { - ...folderForBackend, - folderId: response.data.folderId, +export const updateNewDraftFolder = + ( + folderId: string, + folderDetails: FolderDataFromForm & { folder: Object } + ): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const updatedFolder = _extend( + { ...folderDetails.folder }, + { name: folderDetails.name, description: folderDetails.description } + ) + const changes = [ + { op: "add", path: "/name", value: folderDetails.name }, + { op: "add", path: "/description", value: folderDetails.description }, + ] + const response = await folderAPIService.patchFolderById(folderId, changes) + + return new Promise((resolve, reject) => { + if (response.ok) { + dispatch(setFolder(updatedFolder)) + resolve(response) + } else { + reject(JSON.stringify(response)) } - dispatch(setFolder(folder)) - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const updateNewDraftFolder = ( - folderId: string, - folderDetails: FolderDataFromForm & { folder: Object } -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const updatedFolder = _extend( - { ...folderDetails.folder }, - { name: folderDetails.name, description: folderDetails.description } - ) - const changes = [ - { op: "add", path: "/name", value: folderDetails.name }, - { op: "add", path: "/description", value: folderDetails.description }, - ] - const response = await folderAPIService.patchFolderById(folderId, changes) - - return new Promise((resolve, reject) => { - if (response.ok) { - dispatch(setFolder(updatedFolder)) - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const addObjectToFolder = ( - folderID: string, - objectDetails: ObjectInsideFolderWithTags -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const changes = [{ op: "add", path: "/metadataObjects/-", value: objectDetails }] - const response = await folderAPIService.patchFolderById(folderID, changes) - return new Promise((resolve, reject) => { - if (response.ok) { - dispatch(addObject(objectDetails)) - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const replaceObjectInFolder = ( - folderID: string, - accessionId: string, - index: number, - tags: { submissionType: string, fileName: string } -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const changes = [{ op: "replace", path: `/metadataObjects/${index}/tags`, value: tags }] - const response = await folderAPIService.patchFolderById(folderID, changes) - return new Promise((resolve, reject) => { - if (response.ok) { - dispatch(modifyObjectTags({ accessionId: accessionId, tags: tags })) - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const addObjectToDrafts = ( - folderID: string, - objectDetails: ObjectInsideFolderWithTags -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const changes = [{ op: "add", path: "/drafts/-", value: objectDetails }] - const folderResponse = await folderAPIService.patchFolderById(folderID, changes) - - return new Promise((resolve, reject) => { - if (folderResponse.ok) { - dispatch(addDraftObject(objectDetails)) - resolve(folderResponse) - } else { - reject(JSON.stringify(folderResponse)) - } - }) -} + }) + } + +export const addObjectToFolder = + (folderID: string, objectDetails: ObjectInsideFolderWithTags): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const changes = [{ op: "add", path: "/metadataObjects/-", value: objectDetails }] + const response = await folderAPIService.patchFolderById(folderID, changes) + return new Promise((resolve, reject) => { + if (response.ok) { + dispatch(addObject(objectDetails)) + resolve(response) + } else { + reject(JSON.stringify(response)) + } + }) + } + +export const replaceObjectInFolder = + ( + folderID: string, + accessionId: string, + index: number, + tags: { submissionType?: string, displayTitle?: string, fileName?: string }, + objectStatus?: string + ): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const changes = + objectStatus === ObjectStatus.submitted + ? [{ op: "replace", path: `/metadataObjects/${index}/tags`, value: tags }] + : [{ op: "replace", path: `/drafts/${index}/tags`, value: tags }] + + const response = await folderAPIService.patchFolderById(folderID, changes) + return new Promise((resolve, reject) => { + if (response.ok) { + objectStatus === ObjectStatus.submitted + ? dispatch(modifyObjectTags({ accessionId: accessionId, tags: tags })) + : dispatch( + modifyDraftObjectTags({ + accessionId, + tags, + }) + ) + resolve(response) + } else { + reject(JSON.stringify(response)) + } + }) + } + +export const addObjectToDrafts = + (folderID: string, objectDetails: ObjectInsideFolderWithTags): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const changes = [{ op: "add", path: "/drafts/-", value: objectDetails }] + const folderResponse = await folderAPIService.patchFolderById(folderID, changes) + + return new Promise((resolve, reject) => { + if (folderResponse.ok) { + dispatch(addDraftObject(objectDetails)) + resolve(folderResponse) + } else { + reject(JSON.stringify(folderResponse)) + } + }) + } // Delete object from either metaDataObjects or drafts depending on savedType -export const deleteObjectFromFolder = ( - savedType: string, - objectId: string, - objectType: string -): ((dispatch: (any) => void) => Promise) => async (dispatch: any => void) => { - const service = savedType === ObjectStatus.submitted ? objectAPIService : draftAPIService - const response = await service.deleteObjectByAccessionId(objectType, objectId) - return new Promise((resolve, reject) => { - if (response.ok) { - savedType === ObjectStatus.submitted ? dispatch(deleteObject(objectId)) : dispatch(deleteDraftObject(objectId)) - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const publishFolderContent = (folder: FolderDetailsWithId): (() => Promise) => async () => { - const response = await publishAPIService.publishFolderById(folder.folderId) - return new Promise((resolve, reject) => { - if (response.ok) { - resolve(response) - } else { - reject(JSON.stringify(response)) - } - }) -} - -export const deleteFolderAndContent = (folder: FolderDetailsWithId): (() => Promise) => async () => { - let message = "" - if (folder) { - if (folder.metadataObjects) { - await Promise.all( - folder.metadataObjects.map(async object => { - const response = await objectAPIService.deleteObjectByAccessionId(object.schema, object.accessionId) - if (!response.ok) message = `Couldn't delete object with accessionId ${object.accessionId}` - }) - ) - } +export const deleteObjectFromFolder = + (savedType: string, objectId: string, objectType: string): ((dispatch: (any) => void) => Promise) => + async (dispatch: any => void) => { + const service = savedType === ObjectStatus.submitted ? objectAPIService : draftAPIService + const response = await service.deleteObjectByAccessionId(objectType, objectId) + return new Promise((resolve, reject) => { + if (response.ok) { + savedType === ObjectStatus.submitted ? dispatch(deleteObject(objectId)) : dispatch(deleteDraftObject(objectId)) + resolve(response) + } else { + reject(JSON.stringify(response)) + } + }) + } - const response = await folderAPIService.deleteFolderById(folder.folderId) - if (!response.ok) message = `Couldn't delete folder with id ${folder.folderId}` +export const publishFolderContent = + (folder: FolderDetailsWithId): (() => Promise) => + async () => { + const response = await publishAPIService.publishFolderById(folder.folderId) return new Promise((resolve, reject) => { if (response.ok) { resolve(response) } else { - reject({ response: JSON.stringify(response), message: message }) + reject(JSON.stringify(response)) } }) } -} + +export const deleteFolderAndContent = + (folder: FolderDetailsWithId): (() => Promise) => + async () => { + let message = "" + if (folder) { + if (folder.metadataObjects) { + await Promise.all( + folder.metadataObjects.map(async object => { + const response = await objectAPIService.deleteObjectByAccessionId(object.schema, object.accessionId) + if (!response.ok) message = `Couldn't delete object with accessionId ${object.accessionId}` + }) + ) + } + + const response = await folderAPIService.deleteFolderById(folder.folderId) + if (!response.ok) message = `Couldn't delete folder with id ${folder.folderId}` + return new Promise((resolve, reject) => { + if (response.ok) { + resolve(response) + } else { + reject({ response: JSON.stringify(response), message: message }) + } + }) + } + }