diff --git a/.gitignore b/.gitignore index c7a69e95..b69a434e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ yarn-error.log* # IDE settings .idea/ +.vscode/ diff --git a/cypress/integration/app.spec.js b/cypress/integration/app.spec.js index 3010c975..d99ec9ee 100644 --- a/cypress/integration/app.spec.js +++ b/cypress/integration/app.spec.js @@ -6,7 +6,7 @@ describe("Basic e2e", function () { cy.get('[alt="CSC Login"]').click() }) - it("should create new folder, add study form and publish folder", () => { + it("should create new folder, add Study form, upload Study XML file, add Analysis form and publish folder", () => { cy.visit(baseUrl) cy.get('[alt="CSC Login"]').click() cy.visit(baseUrl + "newdraft") @@ -19,15 +19,17 @@ describe("Basic e2e", function () { cy.get("textarea[name='description']").type("Test description") cy.get("button[type=button]").contains("Next").click() - // Fill a study form and submit object + // Fill a Study form and submit object cy.get("div[role=button]").contains("Study").click() cy.get("div[role=button]").contains("Fill Form").click() cy.get("input[name='descriptor.studyTitle']").type("Test title") cy.get("select[name='descriptor.studyType']").select("Metagenomics") + + // Submit form cy.get("button[type=submit]").contains("Submit").click() cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) - // Upload an xml file. + // Upload a Study xml file. cy.get("div[role=button]").contains("Upload XML File").click() cy.fixture("study_test.xml").then(fileContent => { cy.get('input[type="file"]').attachFile({ @@ -37,14 +39,93 @@ describe("Basic e2e", function () { force: true, }) }) - // Hacky way to get past RHF watch -method problem that doesn't allow cypress to get updated value for file + // Cypress doesn't allow form validation status to update and therefore "send" button is disabled cy.get("form").submit() - // Saved objects list should have newly added item + // Saved objects list should have newly added item from Study object cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 2) - // // Navigate to summary and publish + // Fill an Analysis form and submit object + cy.get("div[role=button]").contains("Analysis").click() + cy.get("div[role=button]") + .contains("Fill Form") + .should("be.visible") + .then($btn => $btn.click()) + + cy.get("form").within(() => { + // Experiment + cy.get("input[name='experimentRef.accessionId']").type("Experiment Test Accession Id") + cy.get("input[name='experimentRef.identifiers.submitterId.namespace']").type("Experiment Test Namespace") + cy.get("input[name='experimentRef.identifiers.submitterId.value']").type("Experiment Test Value") + + // Study + cy.get("input[name='studyRef.accessionId']").type("Study Test Accession Id") + cy.get("input[name='studyRef.identifiers.submitterId.namespace']").type("Study Test Namespace") + cy.get("input[name='studyRef.identifiers.submitterId.value']").type("Study Test Value") + + // Sample + cy.get("input[name='sampleRef.accessionId']").type("Sample Test Accession Id") + cy.get("input[name='sampleRef.identifiers.submitterId.namespace']").type("Sample Test Namespace") + cy.get("input[name='sampleRef.identifiers.submitterId.value']").type("Sample Test Value") + + // Run + cy.get("input[name='runRef.accessionId']").type("Run Test Accession Id") + cy.get("input[name='runRef.identifiers.submitterId.namespace']").type("Run Test Namespace") + cy.get("input[name='runRef.identifiers.submitterId.value']").type("Run Test Value") + + // Analysis + cy.get("input[name='analysisRef.accessionId']").type("Analysis Test Accession Id") + cy.get("input[name='analysisRef.identifiers.submitterId.namespace']").type("Analysis Test Namespace") + cy.get("input[name='analysisRef.identifiers.submitterId.value']").type("Analysis Test Value") + + cy.get("h3") + .contains("Reference Alignment") + .parent("div.formSection") + .within(() => { + cy.get("button").contains("Add new item").click() + cy.get("input[name='analysisType.referenceAlignment.sequence[0].accessionId']").type("Reference Accession Id") + }) + + cy.get("h3") + .contains("Sequence Variation") + .parent("div.formSection") + .within(() => { + cy.get("input[name='analysisType.sequenceVariation.assembly.standard.accessionId']").type( + "Sequence Standard Accession Id" + ) + cy.get("button").contains("Add new item").click() + cy.get("input[name='analysisType.sequenceVariation.sequence[0].accessionId']").type( + "Squence Sequence Accession Id" + ) + }) + + cy.get("h3") + .contains("Processed Reads") + .parent("div.formSection") + .within(() => { + cy.get("input[name='analysisType.processedReads.assembly.standard.accessionId']").type( + "Processed Standard Accession Id" + ) + cy.get("button").contains("Add new item").click() + + cy.get("input[name='analysisType.processedReads.sequence[0].accessionId']").type( + "Processed Sequence Accession Id" + ) + }) + cy.root().submit() + }) + + // Saved objects list should have newly added item from Analysis object + cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) + + // Navigate to summary cy.get("button[type=button]").contains("Next").click() + + // Check the amount of submitted objects in each object type + cy.get("h6").contains("Study").parent("div").children().eq(1).should("have.text", 2) + cy.get("h6").contains("Analysis").parent("div").children().eq(1).should("have.text", 1) + + // Navigate to publish cy.get("button[type=button]").contains("Publish").click() cy.get('button[aria-label="Publish folder contents and move to frontpage"]').contains("Publish").click() }) diff --git a/cypress/integration/draft.spec.js b/cypress/integration/draft.spec.js new file mode 100644 index 00000000..4e364957 --- /dev/null +++ b/cypress/integration/draft.spec.js @@ -0,0 +1,58 @@ +describe("Draft operations", function () { + const baseUrl = "http://localhost:" + Cypress.env("port") + "/" + + it("should create new folder, save, delete and continue draft", () => { + cy.visit(baseUrl) + cy.get('[alt="CSC Login"]').click() + cy.visit(baseUrl + "newdraft") + + // Navigate to folder creation + cy.get("button[type=button]").contains("New folder").click() + + // Add folder name & description, navigate to submissions + cy.get("input[name='name']").type("Test name") + cy.get("textarea[name='description']").type("Test description") + cy.get("button[type=button]").contains("Next").click() + + // Fill a Study form + cy.get("div[role=button]").contains("Study").click() + cy.get("div[role=button]").contains("Fill Form").click() + cy.get("input[name='descriptor.studyTitle']").type("Test title") + + // Save a draft + cy.get("button[type=button]").contains("Save as Draft").click() + cy.get("div[role=alert]", { timeout: 10000 }).contains("Draft saved with") + cy.get("div[role=button]").contains("Choose from drafts").click() + cy.get("div[data-testid='existing']").find("li").should("have.length", 1) + + // Save another draft + cy.get("div[role=button]").contains("Fill Form").click() + cy.get("input[name='descriptor.studyTitle']").type("Test title 2") + cy.get("button[type=button]").contains("Save as Draft").click() + cy.get("div[role=alert]", { timeout: 10000 }).contains("Draft saved with") + + // Update draft, save from dialog + cy.get("input[name='descriptor.studyTitle']").type(" second save") + cy.get("div[role=button]").contains("Choose from drafts").click() + cy.get("h2").contains("Would you like to save draft version of this form") + cy.get("div[role=dialog]").contains("Save").click() + cy.get("div[data-testid='existing']").find("li").should("have.length", 2) + + // Delete a draft + cy.get("button[aria-label='Delete draft']").first().click() + cy.get("div[data-testid='existing']").find("li").should("have.length", 1) + + // Continue draft + cy.get("button[aria-label='Continue draft']").first().click() + cy.get("input[name='descriptor.studyTitle']").should("have.value", "Test title 2 second save") + cy.get("select[name='descriptor.studyType']").select("Metagenomics") + + // Submit form + cy.get("button[type=submit]").contains("Submit").click() + cy.get(".MuiListItem-container", { timeout: 10000 }).should("have.length", 1) + + // Drafts should be empty + cy.get("div[role=button]").contains("Choose from drafts").click() + cy.get("h3").contains("No study drafts.") + }) +}) diff --git a/package-lock.json b/package-lock.json index f4e3c6c2..31e8fe6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "metadata-submitter-frontend", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2704,9 +2704,9 @@ } }, "@testing-library/dom": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.28.1.tgz", - "integrity": "sha512-acv3l6kDwZkQif/YqJjstT3ks5aaI33uxGNVIQmdKzbZ2eMKgg3EV2tB84GDdc72k3Kjhl6mO8yUt6StVIdRDg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.2.tgz", + "integrity": "sha512-CBMELfyY1jKdtLcSRmEnZWRzRkCRVSNPTzhzrn8wY8OnzUo7Pe/W+HgLzt4TDnWIPYeusHBodf9wUjJF48kPmA==", "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2719,17 +2719,17 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" }, "@babel/highlight": { "version": "7.10.4", @@ -2869,9 +2869,9 @@ } }, "@testing-library/react": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.2.tgz", - "integrity": "sha512-jaxm0hwUjv+hzC+UFEywic7buDC9JQ1q3cDsrWVSDAPmLotfA6E6kUHlYm/zOeGCac6g48DR36tFHxl7Zb+N5A==", + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.3.tgz", + "integrity": "sha512-BirBUGPkTW28ULuCwIbYo0y2+0aavHczBT6N9r3LrsswEW3pg25l1wgoE7I8QBIy1upXWkwKpYdWY7NYYP0Bxw==", "requires": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.28.1" @@ -3689,11 +3689,11 @@ } }, "apisauce": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/apisauce/-/apisauce-1.1.2.tgz", - "integrity": "sha512-AqOrOVk71JPSqugA6PdrkE2S0w1GC/f3xPZPMHJ1O+Z73pwT2uoGnr8JbfmB/gvO2cnygYzlBOnkD/mN6W1FMQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/apisauce/-/apisauce-2.0.1.tgz", + "integrity": "sha512-mJBw3pKmtfVoP6oifnf7/iRJQtNkVb6GkYsVOXN2pidootj1mhGBtzYHOX9FVBzAz5QV2GMu8IJtiNIgZ44kHQ==", "requires": { - "axios": "^0.19.0", + "axios": "^0.21.1", "ramda": "^0.25.0" } }, @@ -3988,34 +3988,11 @@ "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==" }, "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { - "follow-redirects": "1.5.10" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } + "follow-redirects": "^1.10.0" } }, "axobject-query": { @@ -6073,9 +6050,9 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, "cypress": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.2.0.tgz", - "integrity": "sha512-m/rkcogYM9MTy8rbsZgyS5wT2L/J+B5V2bY2ztkDNMyqhk/oZgUF4KTWVLzkW2I+scg0iAddca95tLlt7XnAtw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.2.1.tgz", + "integrity": "sha512-OYkSgzA4J4Q7eMjZvNf5qWpBLR4RXrkqjL3UZ1UzGGLAskO0nFTi/RomNTG6TKvL3Zp4tw4zFY1gp5MtmkCZrA==", "dev": true, "requires": { "@cypress/listr-verbose-renderer": "^0.4.1", @@ -7470,9 +7447,9 @@ } }, "eslint-plugin-prettier": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz", - "integrity": "sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", + "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0" @@ -8325,12 +8302,12 @@ } }, "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", + "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", "dev": true, "requires": { - "semver-regex": "^2.0.0" + "semver-regex": "^3.1.2" } }, "flat-cache": { @@ -9171,18 +9148,18 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" }, "husky": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.6.tgz", - "integrity": "sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.7.tgz", + "integrity": "sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw==", "dev": true, "requires": { "chalk": "^4.0.0", "ci-info": "^2.0.0", "compare-versions": "^3.6.0", "cosmiconfig": "^7.0.0", - "find-versions": "^3.2.0", + "find-versions": "^4.0.0", "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", + "pkg-dir": "^5.0.0", "please-upgrade-node": "^3.2.0", "slash": "^3.0.0", "which-pm-runs": "^1.0.0" @@ -9223,12 +9200,12 @@ "dev": true }, "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, @@ -9239,38 +9216,32 @@ "dev": true }, "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9278,12 +9249,12 @@ "dev": true }, "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, "requires": { - "find-up": "^4.0.0" + "find-up": "^5.0.0" } }, "supports-color": { @@ -15629,9 +15600,9 @@ "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==" }, "react-hook-form": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.14.0.tgz", - "integrity": "sha512-yj4aqASmyxFPyDtDLKBae+AazFv5vcC5CEpDlh1+r5k5BTH/J/CTG6q0H5lSinm0B0F6P7oTmXIYB75ZmuQz6g==" + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.14.1.tgz", + "integrity": "sha512-Bf2fWcL2F34hUUqrx1rpgZ8ZErQ4/DvrPs6Je+lUYL59km8PXIQqyCTz/NbggC1RQUj7rChSmTx6eFap+mCttw==" }, "react-is": { "version": "16.13.1", @@ -16626,9 +16597,9 @@ "dev": true }, "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz", + "integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==", "dev": true }, "send": { @@ -20056,6 +20027,12 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index 4e2b3291..64489c9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metadata-submitter-frontend", - "version": "0.6.0", + "version": "0.7.0", "private": true, "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.6", @@ -9,14 +9,14 @@ "@material-ui/lab": "^4.0.0-alpha.57", "@reduxjs/toolkit": "^1.5.0", "@testing-library/jest-dom": "^5.11.8", - "@testing-library/react": "^11.1.1", + "@testing-library/react": "^11.2.3", "ajv": "^6.12.6", - "apisauce": "^1.1.2", + "apisauce": "^2.0.1", "jest-environment-jsdom-sixteen": "^1.0.3", "lodash": "^4.17.20", "react": "^16.14.0", "react-dom": "^16.14.0", - "react-hook-form": "^6.14.0", + "react-hook-form": "^6.14.1", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", "react-scripts": "^4.0.0" @@ -57,17 +57,17 @@ "devDependencies": { "@testing-library/user-event": "^12.6.0", "concurrently": "^5.3.0", - "cypress": "^6.2.0", + "cypress": "^6.2.1", "cypress-file-upload": "^4.1.1", "eslint-config-prettier": "^6.15.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-flowtype": "^5.2.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-prettier": "^3.3.0", + "eslint-plugin-prettier": "^3.3.1", "flow-bin": "^0.126.1", "http-proxy-middleware": "^1.0.6", - "husky": "^4.3.6", + "husky": "^4.3.7", "jest-environment-jsdom-sixteen": "^1.0.3", "prettier": "^2.2.1", "redux-mock-store": "^1.5.4", diff --git a/src/__tests__/WizardDraftObjectPicker.test.js b/src/__tests__/WizardDraftObjectPicker.test.js new file mode 100644 index 00000000..0bc16022 --- /dev/null +++ b/src/__tests__/WizardDraftObjectPicker.test.js @@ -0,0 +1,39 @@ +import React from "react" + +import "@testing-library/jest-dom/extend-expect" +import { render, screen } from "@testing-library/react" +import { Provider } from "react-redux" +import configureStore from "redux-mock-store" +import thunk from "redux-thunk" + +import WizardDraftObjectPicker from "../components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker" + +const middlewares = [thunk] +const mockStore = configureStore(middlewares) + +describe("WizardStepper", () => { + const store = mockStore({ + objectType: "study", + submissionType: "existing", + submissionFolder: { + description: "AWD", + id: "FOL90524783", + name: "Testname", + published: false, + drafts: [ + { accessionId: "TESTID1", schema: "draft-study" }, + { accessionId: "TESTID2", schema: "draft-study" }, + { accessionId: "TESTID3", schema: "draft-sample" }, + ], + }, + }) + + it("should have drafts listed for selected object type", async () => { + render( + + + + ) + expect(screen.getAllByRole("button")).toHaveLength(4) + }) +}) diff --git a/src/__tests__/WizardFillObjectDetailsForm.test.js b/src/__tests__/WizardFillObjectDetailsForm.test.js index 0f3ba226..b0b2ad24 100644 --- a/src/__tests__/WizardFillObjectDetailsForm.test.js +++ b/src/__tests__/WizardFillObjectDetailsForm.test.js @@ -41,9 +41,9 @@ describe("WizardFillObjectDetailsForm", () => { }, }) - localStorage.setItem(`cached_study_schema`, JSON.stringify(schema)) + sessionStorage.setItem(`cached_study_schema`, JSON.stringify(schema)) - it("should create study form from schema in localstorage", async () => { + it("should create study form from schema in sessionStorage", async () => { render( @@ -53,8 +53,8 @@ describe("WizardFillObjectDetailsForm", () => { expect(screen.getByText("Study Description")).toBeDefined() }) - // Note: If this test runs before form creation, form creation fails because getItem spy messes localstorage init somehow - it("should call localstorage", async () => { + // Note: If this test runs before form creation, form creation fails because getItem spy messes sessionStorage init somehow + it("should call sessionStorage", async () => { const spy = jest.spyOn(Storage.prototype, "getItem") render( diff --git a/src/__tests__/WizardObjectIndex.test.js b/src/__tests__/WizardObjectIndex.test.js index 107bda63..d6d9821d 100644 --- a/src/__tests__/WizardObjectIndex.test.js +++ b/src/__tests__/WizardObjectIndex.test.js @@ -14,10 +14,10 @@ describe("WizardObjectIndex", () => { const store = mockStore({ submissionFolder: { drafts: [ - { accessionId: "TESTID1234", schema: "study" }, - { accessionId: "TESTID5678", schema: "study" }, - { accessionId: "TESTID0101", schema: "analysis" }, - { accessionId: "TESTID0202", schema: "experiment" }, + { accessionId: "TESTID1234", schema: "draft-study" }, + { accessionId: "TESTID5678", schema: "draft-study" }, + { accessionId: "TESTID0101", schema: "draft-analysis" }, + { accessionId: "TESTID0202", schema: "draft-experiment" }, ], }, }) diff --git a/src/__tests__/WizardSavedObjectsList.test.js b/src/__tests__/WizardSavedObjectsList.test.js index 39af4aba..39fd2d34 100644 --- a/src/__tests__/WizardSavedObjectsList.test.js +++ b/src/__tests__/WizardSavedObjectsList.test.js @@ -20,7 +20,7 @@ describe("WizardStepper", () => { { accessionId: "EDAG2", schema: "sample" }, ] - it("should have 'Added!' message rendered on item that has 'new' property", () => { + it("should have saved objects listed", () => { render( diff --git a/src/components/NewDraftWizard/WizardComponents/WizardAddObjectCard.js b/src/components/NewDraftWizard/WizardComponents/WizardAddObjectCard.js index 3e17b4f0..5f9b9820 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardAddObjectCard.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardAddObjectCard.js @@ -8,6 +8,7 @@ import CardHeader from "@material-ui/core/CardHeader" import { makeStyles } from "@material-ui/core/styles" import { useDispatch, useSelector } from "react-redux" +import WizardDraftObjectPicker from "components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker" import WizardFillObjectDetailsForm from "components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm" import WizardUploadObjectXMLForm from "components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm" import { resetObjectType } from "features/wizardObjectTypeSlice" @@ -28,7 +29,7 @@ const useStyles = makeStyles(theme => ({ marginTop: "-4px", marginBottom: "-4px", }, - cardContent: { + cardCenterContent: { flexGrow: 1, display: "flex", justifyContent: "center", @@ -97,15 +98,17 @@ const WizardAddObjectCard = () => { testId: "xml", }, existing: { - title: "Choose existing object", - component:
Not implemented yet
, + title: "Choose from drafts", + component: , testId: "existing", }, } return ( - {cards[submissionType]["component"]} + + {cards[submissionType]["component"]} + ) } diff --git a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js index 98648a01..767f6a59 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardAlert.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardAlert.js @@ -10,6 +10,7 @@ import DialogTitle from "@material-ui/core/DialogTitle" import Alert from "@material-ui/lab/Alert" import { useDispatch, useSelector } from "react-redux" +import { resetDraftStatus } from "features/draftStatusSlice" import { setAlert, resetAlert } from "features/wizardAlertSlice" import { resetDraftObject } from "features/wizardDraftObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" @@ -17,8 +18,8 @@ import { addObjectToDrafts } from "features/wizardSubmissionFolderSlice" import draftAPIService from "services/draftAPI" // Simple template for error messages -const ErrorMessage = () => { - return Connection error, cannot save draft. +const ErrorMessage = message => { + return {message} } /* @@ -39,31 +40,53 @@ const CancelFormDialog = ({ const draftObject = useSelector(state => state.draftObject) const objectType = useSelector(state => state.objectType) const [error, setError] = useState(false) + const [errorMessage, setErrorMessage] = useState("") const dispatch = useDispatch() - // Draft save logic. Get response depending on submission type + // Draft save logic const saveDraft = async () => { setError(false) - const response = await draftAPIService.createFromJSON(objectType, draftObject) - dispatch( - updateStatus({ - successStatus: "success", - response: response, - errorPrefix: "", - }) - ) - - if (response.ok) { - dispatch( - addObjectToDrafts(submissionFolder.id, { - accessionId: response.data.accessionId, - schema: objectType, - }) - ) - dispatch(resetDraftObject()) - handleDialog(true) + const err = "Connection error, cannot save draft." + if (draftObject.draftId) { + const response = await draftAPIService.patchFromJSON(objectType, draftObject.draftId, draftObject) + if (response.ok) { + dispatch(resetDraftStatus()) + dispatch( + updateStatus({ + successStatus: "success", + response: response, + errorPrefix: "", + }) + ) + dispatch(resetDraftObject()) + handleDialog(true) + } else { + setError(true) + setErrorMessage(err) + } } else { - setError(true) + const response = await draftAPIService.createFromJSON(objectType, draftObject) + if (response.ok) { + dispatch( + updateStatus({ + successStatus: "success", + response: response, + errorPrefix: "", + }) + ) + dispatch(resetDraftStatus()) + dispatch( + addObjectToDrafts(submissionFolder.id, { + accessionId: response.data.accessionId, + schema: "draft-" + objectType, + }) + ) + dispatch(resetDraftObject()) + handleDialog(true) + } else { + setError(true) + setErrorMessage(err) + } } } @@ -233,7 +256,7 @@ const CancelFormDialog = ({ {dialogContent} - {error && } + {error && } {dialogActions} ) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js new file mode 100644 index 00000000..1485ffb8 --- /dev/null +++ b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js @@ -0,0 +1,118 @@ +//@flow +import React, { useState } from "react" + +import Button from "@material-ui/core/Button" +import ButtonGroup from "@material-ui/core/ButtonGroup" +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 { useSelector, useDispatch } from "react-redux" + +import WizardStatusMessageHandler from "../WizardForms/WizardStatusMessageHandler" + +import { setDraftObject } from "features/wizardDraftObjectSlice" +import { deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" +import { setSubmissionType } from "features/wizardSubmissionTypeSlice" +import draftAPIService from "services/draftAPI" + +const useStyles = makeStyles(theme => ({ + objectList: { + padding: "0 1rem", + width: "100%", + }, + objectListItems: { + border: "none", + borderRadius: 3, + margin: theme.spacing(1, 0), + boxShadow: "0px 3px 10px -5px rgba(0,0,0,0.49)", + alignItems: "flex-start", + padding: ".5rem", + }, + buttonContinue: { + color: "#007bff", + }, + buttonDelete: { + color: "#dc3545", + }, +})) + +/** + * List drafts by submission type. Enables fetch and deletion of drafts + */ +const WizardDraftObjectPicker = () => { + const classes = useStyles() + const dispatch = useDispatch() + const objectType = useSelector(state => state.objectType) + const folder = useSelector(state => state.submissionFolder) + const currentObjectTypeDrafts = folder.drafts.filter(draft => draft.schema === "draft-" + objectType) + + const [connError, setConnError] = useState(false) + const [responseError, setResponseError] = useState({}) + const [errorPrefix, setErrorPrefix] = useState("") + + const handleObjectContinue = async objectId => { + setConnError(false) + const response = await draftAPIService.getObjectByAccessionId(objectType, objectId) + if (response.ok) { + dispatch(setDraftObject(response.data)) + dispatch(setSubmissionType("form")) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Draft fetching error.") + } + } + + const handleObjectDelete = objectId => { + setConnError(false) + dispatch(deleteObjectFromFolder("draft", objectId, objectType)).catch(error => { + setConnError(true) + setResponseError(JSON.parse(error)) + setErrorPrefix("Can't delete draft") + }) + } + + return ( +
+ {currentObjectTypeDrafts.length > 0 ? ( + + {currentObjectTypeDrafts.map(submission => { + return ( + + + + + + + + + + ) + })} + + ) : ( +

No {objectType} drafts.

+ )} + + {connError && ( + + )} +
+ ) +} + +export default WizardDraftObjectPicker diff --git a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js index 5d694951..af114625 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js @@ -16,6 +16,7 @@ import { useDispatch, useSelector } from "react-redux" import WizardAlert from "./WizardAlert" import { resetDraftStatus } from "features/draftStatusSlice" +import { resetDraftObject } from "features/wizardDraftObjectSlice" import { setObjectType } from "features/wizardObjectTypeSlice" import { setSubmissionType } from "features/wizardSubmissionTypeSlice" @@ -116,7 +117,7 @@ const SubmissionTypeList = ({ const submissionTypeMap = { form: "Fill Form", xml: "Upload XML File", - existing: "Choose existing object", + existing: "Choose from drafts", } const classes = useStyles() @@ -177,6 +178,7 @@ const WizardObjectIndex = () => { setClickedSubmissionType(submissionType) setCancelFormOpen(true) } else { + dispatch(resetDraftObject()) dispatch(resetDraftStatus()) dispatch(setSubmissionType(submissionType)) dispatch(setObjectType(expandedObjectType)) @@ -212,7 +214,11 @@ const WizardObjectIndex = () => { id="type-header" > {typeCapitalized} - + { setConnError(false) - dispatch(deleteObjectFromFolder(objectId, objectType)).catch(error => { + dispatch(deleteObjectFromFolder("submitted", objectId, objectType)).catch(error => { setConnError(true) setResponseError(JSON.parse(error)) setErrorPrefix("Can't delete object") diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index 741d867d..f7a41e05 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -18,7 +18,7 @@ import WizardStatusMessageHandler from "./WizardStatusMessageHandler" import { setDraftStatus, resetDraftStatus } from "features/draftStatusSlice" import { setDraftObject } from "features/wizardDraftObjectSlice" import { updateStatus } from "features/wizardStatusMessageSlice" -import { addObjectToFolder, addObjectToDrafts } from "features/wizardSubmissionFolderSlice" +import { addObjectToFolder, addObjectToDrafts, deleteObjectFromFolder } from "features/wizardSubmissionFolderSlice" import draftAPIService from "services/draftAPI" import objectAPIService from "services/objectAPI" import schemaAPIService from "services/schemaAPI" @@ -81,33 +81,45 @@ type FormContentProps = { } /* - * Return react-hook-form based form which is rendered from schema and checked against resolver + * 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 classes = useStyles() - const methods = useForm({ mode: "onBlur", resolver }) 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 dispatch = useDispatch() const [cleanedValues, setCleanedValues] = useState({}) + const [currentDraftId, setCurrentDraftId] = useState(draftObject?.accessionId) const [timer, setTimer] = useState(0) const increment = useRef(null) - const alert = useSelector(state => state.alert) const resetForm = () => { methods.reset() } + const checkDirty = () => { + if (methods.formState.isDirty && draftStatus === "") { + dispatch(setDraftStatus("notSaved")) + } + } + useEffect(() => { - methods.formState.isDirty ? dispatch(setDraftStatus("notSaved")) : dispatch(resetDraftStatus()) + checkDirty() }, [methods.formState.isDirty]) const handleChange = () => { - setCleanedValues(JSONSchemaParser.cleanUpFormValues(methods.getValues())) - dispatch(setDraftObject(cleanedValues)) + const values = JSONSchemaParser.cleanUpFormValues(methods.getValues()) + setCleanedValues(values) + dispatch(setDraftObject(Object.assign(values, { draftId: currentDraftId }))) + checkDirty() + } - if (methods.formState.isDirty && draftStatus === "") { - dispatch(setDraftStatus("notSaved")) - } + const handleDraftDelete = draftId => { + dispatch(deleteObjectFromFolder("draft", draftId, objectType)) + setCurrentDraftId(() => null) + handleChange() } /* @@ -137,33 +149,65 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F }, []) const saveDraft = async () => { - const response = await draftAPIService.createFromJSON(objectType, cleanedValues) - if (response.ok) { - dispatch(resetDraftStatus()) - dispatch( - addObjectToDrafts(folderId, { - accessionId: response.data.accessionId, - schema: objectType, - }) - ) - .then(() => { - dispatch( - updateStatus({ - successStatus: "success", - response: response, - errorPrefix: "", - }) - ) - }) - .catch(error => { - dispatch( - updateStatus({ - successStatus: "error", - response: error, - errorPrefix: "Cannot connect to folder API", - }) - ) - }) + 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", + }) + ) + } + } else { + const response = await draftAPIService.createFromJSON(objectType, cleanedValues) + if (response.ok) { + setCurrentDraftId(response.data.accessionId) + dispatch(resetDraftStatus()) + dispatch( + addObjectToDrafts(folderId, { + accessionId: response.data.accessionId, + schema: "draft-" + objectType, + }) + ) + .then(() => { + dispatch( + updateStatus({ + successStatus: "success", + response: response, + errorPrefix: "", + }) + ) + }) + .catch(error => { + dispatch( + updateStatus({ + successStatus: "error", + response: error, + errorPrefix: "Cannot connect to folder API", + }) + ) + }) + } else { + dispatch( + updateStatus({ + successStatus: "error", + response: response, + errorPrefix: "Unexpected error", + }) + ) + } } } @@ -204,7 +248,17 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId }: F > Clear form - @@ -270,13 +324,13 @@ const WizardFillObjectDetailsForm = () => { */ useEffect(() => { const fetchSchema = async () => { - let schema = localStorage.getItem(`cached_${objectType}_schema`) + let schema = sessionStorage.getItem(`cached_${objectType}_schema`) if (!schema || !new Ajv().validateSchema(JSON.parse(schema))) { const response = await schemaAPIService.getSchemaByObjectType(objectType) setResponseInfo(response) if (response.ok) { schema = response.data - localStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(schema)) + sessionStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(schema)) } else { setError(true) setErrorPrefix("Unfortunately an error happened while catching form fields") diff --git a/src/components/NewDraftWizard/WizardSteps/WizardCreateFolderStep.js b/src/components/NewDraftWizard/WizardSteps/WizardCreateFolderStep.js index 4d8d9bb7..a92bd1f2 100644 --- a/src/components/NewDraftWizard/WizardSteps/WizardCreateFolderStep.js +++ b/src/components/NewDraftWizard/WizardSteps/WizardCreateFolderStep.js @@ -39,13 +39,13 @@ const CreateFolderForm = ({ createFolderFormRef }: { createFolderFormRef: Create const onSubmit = data => { setConnError(false) - if (folder) { - dispatch(updateNewDraftFolder(Object.assign({ ...data, folder }))).then(dispatch(increment())) + if (folder && (folder.name !== data.name || folder.description !== data.description)) { + dispatch(updateNewDraftFolder(folder.id, Object.assign({ ...data, folder }))) + .then(() => dispatch(increment())) + .catch(() => setConnError(true)) } else { dispatch(createNewDraftFolder(data)) - .then(() => { - dispatch(increment()) - }) + .then(() => dispatch(increment())) .catch(error => { setConnError(true) setResponseError(JSON.parse(error)) @@ -63,7 +63,7 @@ const CreateFolderForm = ({ createFolderFormRef }: { createFolderFormRef: Create fullWidth inputRef={register({ required: true, validate: { name: value => value.length > 0 } })} helperText={errors.name ? "Please give a name for folder." : null} - error={errors.name} + error={errors.name ? true : false} disabled={isSubmitting} defaultValue={folder ? folder.name : ""} > @@ -76,7 +76,7 @@ const CreateFolderForm = ({ createFolderFormRef }: { createFolderFormRef: Create rows={5} inputRef={register({ required: true, validate: { description: value => value.length > 0 } })} helperText={errors.description ? "Please give a description for folder." : null} - error={errors.description} + error={errors.description ? true : false} disabled={isSubmitting} defaultValue={folder ? folder.description : ""} > diff --git a/src/features/wizardSubmissionFolderSlice.js b/src/features/wizardSubmissionFolderSlice.js index 7594cb2e..8f30c9da 100644 --- a/src/features/wizardSubmissionFolderSlice.js +++ b/src/features/wizardSubmissionFolderSlice.js @@ -3,6 +3,7 @@ import { createSlice } from "@reduxjs/toolkit" import _extend from "lodash/extend" import _reject from "lodash/reject" +import draftAPIService from "../services/draftAPI" import objectAPIService from "../services/objectAPI" import folderAPIService from "services/folderAPI" @@ -11,7 +12,7 @@ import publishAPIService from "services/publishAPI" const initialState = null const wizardSubmissionFolderSlice = createSlice({ - name: "wizardStep", + name: "folder", initialState, reducers: { setFolder: (state, action) => action.payload, @@ -54,6 +55,7 @@ type ObjectInFolder = { accessionId: string, schema: string, } + type FolderNoId = { name: string, description: string, @@ -86,12 +88,27 @@ export const createNewDraftFolder = (folderDetails: FolderFromForm) => async (di }) } -export const updateNewDraftFolder = (folderDetails: FolderFromForm) => async (dispatch: any => void) => { +export const updateNewDraftFolder = (folderId: string, folderDetails: FolderFromForm) => async ( + dispatch: any => void +) => { const updatedFolder = _extend( { ...folderDetails.folder }, { name: folderDetails.name, description: folderDetails.description } ) - dispatch(setFolder(updatedFolder)) + 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: ObjectInFolder) => async (dispatch: any => void) => { @@ -121,11 +138,15 @@ export const addObjectToDrafts = (folderID: string, objectDetails: ObjectInFolde }) } -export const deleteObjectFromFolder = (objectId: string, objectType: string) => async (dispatch: any => void) => { - const response = await objectAPIService.deleteObjectByAccessionId(objectType, objectId) +// Delete object from either metaDataObjects or drafts depending on savedType +export const deleteObjectFromFolder = (savedType: string, objectId: string, objectType: string) => async ( + dispatch: any => void +) => { + const service = savedType === "submitted" ? objectAPIService : draftAPIService + const response = await service.deleteObjectByAccessionId(objectType, objectId) return new Promise((resolve, reject) => { if (response.ok) { - dispatch(deleteObject(objectId)) + savedType === "submitted" ? dispatch(deleteObject(objectId)) : dispatch(deleteDraftObject(objectId)) resolve(response) } else { reject(JSON.stringify(response)) diff --git a/src/services/draftAPI.js b/src/services/draftAPI.js index 9780f46c..e62ee969 100644 --- a/src/services/draftAPI.js +++ b/src/services/draftAPI.js @@ -1,7 +1,10 @@ //@flow import { create } from "apisauce" +import { errorMonitor } from "./errorMonitor" + const api = create({ baseURL: "/drafts" }) +api.addMonitor(errorMonitor) const createFromJSON = async (objectType: string, JSONContent: any) => { return await api.post(`/${objectType}`, JSONContent) @@ -11,6 +14,10 @@ const getObjectByAccessionId = async (objectType: string, accessionId: string) = return await api.get(`/${objectType}/${accessionId}`) } +const patchFromJSON = async (objectType: string, accessionId: any, JSONContent: any) => { + return await api.patch(`/${objectType}/${accessionId}`, JSONContent) +} + const getAllObjectsByObjectType = async (objectType: string) => { return await api.get(`/${objectType}`) } @@ -22,6 +29,7 @@ const deleteObjectByAccessionId = async (objectType: string, accessionId: string export default { createFromJSON, getObjectByAccessionId, + patchFromJSON, getAllObjectsByObjectType, deleteObjectByAccessionId, } diff --git a/src/setupTests.js b/src/setupTests.js index 502e50cf..1b610624 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -1,6 +1,6 @@ -const localStorageMock = { +const sessionStorageMock = { getItem: jest.fn(), setItem: jest.fn(), clear: jest.fn(), } -global.localStorage = localStorageMock +global.sessionStorage = sessionStorageMock