diff --git a/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx index cea46386a..6a2d6ab77 100644 --- a/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx @@ -48,7 +48,7 @@ describe('Ingestion', () => { it('should show the dialog', () => { cy.login('mocked').visit(PATH); - cy.contains('button', 'Move To Extraction Phase').click(); + cy.contains('button', 'go to extraction').click(); cy.contains('button', 'extraction: get started').click(); cy.contains('button', 'NEXT').click(); diff --git a/compose/neurosynth-frontend/jest.config.js b/compose/neurosynth-frontend/jest.config.js index 60ab61a7f..c5657099a 100644 --- a/compose/neurosynth-frontend/jest.config.js +++ b/compose/neurosynth-frontend/jest.config.js @@ -12,4 +12,5 @@ module.exports = { roots: [''], modulePaths: [''], testEnvironment: 'jsdom', + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], }; diff --git a/compose/neurosynth-frontend/package-lock.json b/compose/neurosynth-frontend/package-lock.json index ffcd3ec47..d0b4d09bb 100644 --- a/compose/neurosynth-frontend/package-lock.json +++ b/compose/neurosynth-frontend/package-lock.json @@ -9,9 +9,12 @@ "version": "0.1.0", "dependencies": { "@auth0/auth0-react": "^1.6.0", - "@citation-js/plugin-bibtex": "^0.6.6", - "@citation-js/plugin-enw": "^0.1.1", - "@citation-js/plugin-ris": "^0.6.5", + "@citation-js/core": "^0.7.14", + "@citation-js/plugin-bibjson": "^0.7.14", + "@citation-js/plugin-bibtex": "^0.7.14", + "@citation-js/plugin-doi": "^0.7.14", + "@citation-js/plugin-enw": "^0.3.0", + "@citation-js/plugin-ris": "^0.7.14", "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@handsontable/react": "^12.3.3", @@ -30,7 +33,6 @@ "@types/react-window": "^1.8.5", "@types/uuid": "^9.0.0", "axios": "^0.28.0", - "citation-js": "^0.6.7", "fast-xml-parser": "^4.2.5", "handsontable": "^12.3.3", "html-to-image": "^1.11.11", @@ -2124,31 +2126,10 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, - "node_modules/@citation-js/cli": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/cli/-/cli-0.6.9.tgz", - "integrity": "sha512-MPmaPmfZem1ZsfcrNRtAZX2IIpInB9Mod6k5GbNudF92vu96yz6dBgOXLFKZqxDGAp/bi2v1g+0LxEpbTeEiZQ==", - "dependencies": { - "@citation-js/core": "^0.6.9", - "@citation-js/plugin-bibjson": "^0.6.9", - "@citation-js/plugin-bibtex": "^0.6.9", - "@citation-js/plugin-csl": "^0.6.9", - "@citation-js/plugin-doi": "^0.6.9", - "@citation-js/plugin-ris": "^0.6.9", - "@citation-js/plugin-wikidata": "^0.6.9", - "commander": "^10.0.1" - }, - "bin": { - "citation-js": "lib/index.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@citation-js/core": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.9.tgz", - "integrity": "sha512-ElG4cvedkaRm/a40yhWXOUPUzAImwqI7ZmIyL55GIR4EmfQ5PkaSEcFhA3dekMa66l52ddn4lQaNVO+/yECq9A==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", + "integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", @@ -2156,7 +2137,7 @@ "sync-fetch": "^0.4.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@citation-js/date": { @@ -2176,109 +2157,78 @@ } }, "node_modules/@citation-js/plugin-bibjson": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.6.9.tgz", - "integrity": "sha512-AJI9kiMLeFm7tQ8mPXJ+DpABB0e9ZN8nchhxNnWcfy3Q5RB/i76RF6S/QEsDcQ4vdP4nWmBsKT9UmKdWkFrOQA==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.14.tgz", + "integrity": "sha512-Hcmk01KrpHwcl5uVoLE6TRaJRFg7/qUvpJDcKqx3LLLCsNbaBlISfRDeFETrjjipTetkX70RvtS7FfGUN58gCQ==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@citation-js/core": "^0.6.0" + "@citation-js/core": "^0.7.0" } }, "node_modules/@citation-js/plugin-bibtex": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.9.tgz", - "integrity": "sha512-35xHut8rbDAN2ffYt+BPn+DChxXhbVnN86fu2DHDQNWlCqCrUJhQTMRMlD0L1uuyHTDrkTvgLPtcs14WC+xKzA==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.14.tgz", + "integrity": "sha512-xHOHqhF6dthLRv46N9U+mQgYLiiWQHLvQWK9+mcBKz+/3NWge62Xb1oBouNWwLEPd5FV/8gp9fp7SOp93T0dUg==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", "moo": "^0.5.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@citation-js/core": "^0.6.0" - } - }, - "node_modules/@citation-js/plugin-csl": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.9.tgz", - "integrity": "sha512-W8AYDK5o6qgkdBWgXPXmpzbTUxyfoADDbiGf1BqMdeD0IH3kSmgvXcsyeh8NMxiaM6B5VDg0k0pHaaxbTm5POA==", - "dependencies": { - "@citation-js/date": "^0.5.0", - "citeproc": "^2.4.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@citation-js/core": "^0.6.0" + "@citation-js/core": "^0.7.0" } }, "node_modules/@citation-js/plugin-doi": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-doi/-/plugin-doi-0.6.9.tgz", - "integrity": "sha512-URU6z20r0O26qDBuqqT6zHRs0Kr7vUcL+hkayYV1b2Wnhjr0qLW3mxjUhcFEDO8lnKsdEBrXuZ6TBK1q+yI7CQ==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-doi/-/plugin-doi-0.7.14.tgz", + "integrity": "sha512-U/E9HrGcxXr6u8R0+Ivlb7SE0lNf+DzjmlArNSh+9B1iaakjy/PzI06Myvf8jQqWBhBD0JLl2xJvrwYK6iiD3A==", "dependencies": { "@citation-js/date": "^0.5.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@citation-js/core": "^0.6.0" + "@citation-js/core": "^0.7.0" } }, "node_modules/@citation-js/plugin-enw": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-enw/-/plugin-enw-0.1.1.tgz", - "integrity": "sha512-xCXbm/EXyu9ygKTV8qKXLW77lggHO6IeAui5ZhgEL9c09be4t7co9DN4vDPpKJSHKxY0nSqiPmtbvJDyic5iEg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-enw/-/plugin-enw-0.3.0.tgz", + "integrity": "sha512-TT1GrWCuLNC9N2uXXjFfLO/0tBwgcO2YQIJuUrcFElV85hj9uvxpACQopUb9WXoyWwSmHsyG9dFWiztJrwaURg==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" }, "engines": { - "node": ">=14" + "node": ">=16" }, "peerDependencies": { - "@citation-js/core": "^0.6.0" + "@citation-js/core": "^0.7.1" } }, "node_modules/@citation-js/plugin-ris": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.6.9.tgz", - "integrity": "sha512-ADoq3bEwh3KG1P1/m3dw4VN8n04oLh45o6a5x27tfqpL+nva47An0rSPjNkOAmKCAQQjOaZOtHnQahG1jjFPUw==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.7.14.tgz", + "integrity": "sha512-FZz1fa9dOrLAV/8NXgnqK1PfNKW/pOAXj1WDwoyGBrG+mbLtAwvTNhHDHGS5LvPoBbizqWkx8M8dYagke+FGOg==", "dependencies": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@citation-js/core": "^0.6.0" - } - }, - "node_modules/@citation-js/plugin-wikidata": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-wikidata/-/plugin-wikidata-0.6.9.tgz", - "integrity": "sha512-SOpGhAtaBIbJYkf5+6eyh4fmED/WLjIhWorFOuWAtahjeU09L73hVwFffVbk6kxVfZm7OyH5RD2nhPmQLXrLCA==", - "dependencies": { - "@citation-js/date": "^0.5.0", - "@citation-js/name": "^0.4.2", - "wikidata-sdk": "^8.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@citation-js/core": "^0.6.0" + "@citation-js/core": "^0.7.0" } }, "node_modules/@colors/colors": { @@ -8218,35 +8168,6 @@ "node": ">=8" } }, - "node_modules/citation-js": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/citation-js/-/citation-js-0.6.9.tgz", - "integrity": "sha512-BaFBfaZXTbCdhYT2zTc4dpU6ktDnfPTLAQ2NYY3rZifL2/SdJ0/nFymmPtF4gUQxYL/qC8tyj3dEe3SRH6gAtQ==", - "dependencies": { - "@citation-js/cli": "0.6.9", - "@citation-js/core": "0.6.9", - "@citation-js/date": "0.5.1", - "@citation-js/name": "0.4.2", - "@citation-js/plugin-bibjson": "0.6.9", - "@citation-js/plugin-bibtex": "0.6.9", - "@citation-js/plugin-csl": "0.6.9", - "@citation-js/plugin-doi": "0.6.9", - "@citation-js/plugin-ris": "0.6.9", - "@citation-js/plugin-wikidata": "0.6.9", - "citeproc": "^2.4.59" - }, - "bin": { - "citation-js": "bin/cmd.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/citeproc": { - "version": "2.4.63", - "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", - "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==" - }, "node_modules/cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", @@ -8480,14 +8401,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -24939,26 +24852,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wikibase-sdk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/wikibase-sdk/-/wikibase-sdk-8.1.1.tgz", - "integrity": "sha512-1NjMnfNQ4OaLh0dFAeTMvV3vGAq6HXsNKGfYUJYOVyBPGBDMunlY3QZ8+72hLV5FiKmc6Bzg1xbI0jCHfHmIew==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/wikidata-sdk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/wikidata-sdk/-/wikidata-sdk-8.1.1.tgz", - "integrity": "sha512-KOUhJtpCHg32k/tz7pFj/BaqiYAvYGz4sGzcT15t120832WtfNTGCSeohmln40/JOjpxzfzud/6q2x6Q0Ji5hg==", - "deprecated": "wikidata-sdk has been renamed wikibase-sdk", - "dependencies": { - "wikibase-sdk": "^8.1.1" - }, - "engines": { - "node": ">= 6.4" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -26866,25 +26759,10 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, - "@citation-js/cli": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/cli/-/cli-0.6.9.tgz", - "integrity": "sha512-MPmaPmfZem1ZsfcrNRtAZX2IIpInB9Mod6k5GbNudF92vu96yz6dBgOXLFKZqxDGAp/bi2v1g+0LxEpbTeEiZQ==", - "requires": { - "@citation-js/core": "^0.6.9", - "@citation-js/plugin-bibjson": "^0.6.9", - "@citation-js/plugin-bibtex": "^0.6.9", - "@citation-js/plugin-csl": "^0.6.9", - "@citation-js/plugin-doi": "^0.6.9", - "@citation-js/plugin-ris": "^0.6.9", - "@citation-js/plugin-wikidata": "^0.6.9", - "commander": "^10.0.1" - } - }, "@citation-js/core": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.9.tgz", - "integrity": "sha512-ElG4cvedkaRm/a40yhWXOUPUzAImwqI7ZmIyL55GIR4EmfQ5PkaSEcFhA3dekMa66l52ddn4lQaNVO+/yECq9A==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", + "integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==", "requires": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", @@ -26903,69 +26781,50 @@ "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==" }, "@citation-js/plugin-bibjson": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.6.9.tgz", - "integrity": "sha512-AJI9kiMLeFm7tQ8mPXJ+DpABB0e9ZN8nchhxNnWcfy3Q5RB/i76RF6S/QEsDcQ4vdP4nWmBsKT9UmKdWkFrOQA==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.14.tgz", + "integrity": "sha512-Hcmk01KrpHwcl5uVoLE6TRaJRFg7/qUvpJDcKqx3LLLCsNbaBlISfRDeFETrjjipTetkX70RvtS7FfGUN58gCQ==", "requires": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" } }, "@citation-js/plugin-bibtex": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.9.tgz", - "integrity": "sha512-35xHut8rbDAN2ffYt+BPn+DChxXhbVnN86fu2DHDQNWlCqCrUJhQTMRMlD0L1uuyHTDrkTvgLPtcs14WC+xKzA==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.14.tgz", + "integrity": "sha512-xHOHqhF6dthLRv46N9U+mQgYLiiWQHLvQWK9+mcBKz+/3NWge62Xb1oBouNWwLEPd5FV/8gp9fp7SOp93T0dUg==", "requires": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2", "moo": "^0.5.1" } }, - "@citation-js/plugin-csl": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.9.tgz", - "integrity": "sha512-W8AYDK5o6qgkdBWgXPXmpzbTUxyfoADDbiGf1BqMdeD0IH3kSmgvXcsyeh8NMxiaM6B5VDg0k0pHaaxbTm5POA==", - "requires": { - "@citation-js/date": "^0.5.0", - "citeproc": "^2.4.6" - } - }, "@citation-js/plugin-doi": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-doi/-/plugin-doi-0.6.9.tgz", - "integrity": "sha512-URU6z20r0O26qDBuqqT6zHRs0Kr7vUcL+hkayYV1b2Wnhjr0qLW3mxjUhcFEDO8lnKsdEBrXuZ6TBK1q+yI7CQ==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-doi/-/plugin-doi-0.7.14.tgz", + "integrity": "sha512-U/E9HrGcxXr6u8R0+Ivlb7SE0lNf+DzjmlArNSh+9B1iaakjy/PzI06Myvf8jQqWBhBD0JLl2xJvrwYK6iiD3A==", "requires": { "@citation-js/date": "^0.5.0" } }, "@citation-js/plugin-enw": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-enw/-/plugin-enw-0.1.1.tgz", - "integrity": "sha512-xCXbm/EXyu9ygKTV8qKXLW77lggHO6IeAui5ZhgEL9c09be4t7co9DN4vDPpKJSHKxY0nSqiPmtbvJDyic5iEg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-enw/-/plugin-enw-0.3.0.tgz", + "integrity": "sha512-TT1GrWCuLNC9N2uXXjFfLO/0tBwgcO2YQIJuUrcFElV85hj9uvxpACQopUb9WXoyWwSmHsyG9dFWiztJrwaURg==", "requires": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" } }, "@citation-js/plugin-ris": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.6.9.tgz", - "integrity": "sha512-ADoq3bEwh3KG1P1/m3dw4VN8n04oLh45o6a5x27tfqpL+nva47An0rSPjNkOAmKCAQQjOaZOtHnQahG1jjFPUw==", + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.7.14.tgz", + "integrity": "sha512-FZz1fa9dOrLAV/8NXgnqK1PfNKW/pOAXj1WDwoyGBrG+mbLtAwvTNhHDHGS5LvPoBbizqWkx8M8dYagke+FGOg==", "requires": { "@citation-js/date": "^0.5.0", "@citation-js/name": "^0.4.2" } }, - "@citation-js/plugin-wikidata": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-wikidata/-/plugin-wikidata-0.6.9.tgz", - "integrity": "sha512-SOpGhAtaBIbJYkf5+6eyh4fmED/WLjIhWorFOuWAtahjeU09L73hVwFffVbk6kxVfZm7OyH5RD2nhPmQLXrLCA==", - "requires": { - "@citation-js/date": "^0.5.0", - "@citation-js/name": "^0.4.2", - "wikidata-sdk": "^8.0.0" - } - }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -31263,29 +31122,6 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" }, - "citation-js": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/citation-js/-/citation-js-0.6.9.tgz", - "integrity": "sha512-BaFBfaZXTbCdhYT2zTc4dpU6ktDnfPTLAQ2NYY3rZifL2/SdJ0/nFymmPtF4gUQxYL/qC8tyj3dEe3SRH6gAtQ==", - "requires": { - "@citation-js/cli": "0.6.9", - "@citation-js/core": "0.6.9", - "@citation-js/date": "0.5.1", - "@citation-js/name": "0.4.2", - "@citation-js/plugin-bibjson": "0.6.9", - "@citation-js/plugin-bibtex": "0.6.9", - "@citation-js/plugin-csl": "0.6.9", - "@citation-js/plugin-doi": "0.6.9", - "@citation-js/plugin-ris": "0.6.9", - "@citation-js/plugin-wikidata": "0.6.9", - "citeproc": "^2.4.59" - } - }, - "citeproc": { - "version": "2.4.63", - "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", - "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==" - }, "cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", @@ -31460,11 +31296,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" - }, "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -43042,19 +42873,6 @@ } } }, - "wikibase-sdk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/wikibase-sdk/-/wikibase-sdk-8.1.1.tgz", - "integrity": "sha512-1NjMnfNQ4OaLh0dFAeTMvV3vGAq6HXsNKGfYUJYOVyBPGBDMunlY3QZ8+72hLV5FiKmc6Bzg1xbI0jCHfHmIew==" - }, - "wikidata-sdk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/wikidata-sdk/-/wikidata-sdk-8.1.1.tgz", - "integrity": "sha512-KOUhJtpCHg32k/tz7pFj/BaqiYAvYGz4sGzcT15t120832WtfNTGCSeohmln40/JOjpxzfzud/6q2x6Q0Ji5hg==", - "requires": { - "wikibase-sdk": "^8.1.1" - } - }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/compose/neurosynth-frontend/package.json b/compose/neurosynth-frontend/package.json index dabcb286d..44e86ef18 100644 --- a/compose/neurosynth-frontend/package.json +++ b/compose/neurosynth-frontend/package.json @@ -4,9 +4,12 @@ "private": true, "dependencies": { "@auth0/auth0-react": "^1.6.0", - "@citation-js/plugin-bibtex": "^0.6.6", - "@citation-js/plugin-enw": "^0.1.1", - "@citation-js/plugin-ris": "^0.6.5", + "@citation-js/core": "^0.7.14", + "@citation-js/plugin-bibjson": "^0.7.14", + "@citation-js/plugin-bibtex": "^0.7.14", + "@citation-js/plugin-doi": "^0.7.14", + "@citation-js/plugin-enw": "^0.3.0", + "@citation-js/plugin-ris": "^0.7.14", "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@handsontable/react": "^12.3.3", @@ -25,7 +28,6 @@ "@types/react-window": "^1.8.5", "@types/uuid": "^9.0.0", "axios": "^0.28.0", - "citation-js": "^0.6.7", "fast-xml-parser": "^4.2.5", "handsontable": "^12.3.3", "html-to-image": "^1.11.11", diff --git a/compose/neurosynth-frontend/src/__mocks__/react-query/useIsFetching.ts b/compose/neurosynth-frontend/src/__mocks__/react-query/useIsFetching.ts deleted file mode 100644 index 539c2cd2d..000000000 --- a/compose/neurosynth-frontend/src/__mocks__/react-query/useIsFetching.ts +++ /dev/null @@ -1,3 +0,0 @@ -const mockUseIsFetching = jest.fn().mockReturnValue(0); - -export { mockUseIsFetching }; diff --git a/compose/neurosynth-frontend/src/__mocks__/react-query/useMutation.ts b/compose/neurosynth-frontend/src/__mocks__/react-query/useMutation.ts deleted file mode 100644 index fdf3651e2..000000000 --- a/compose/neurosynth-frontend/src/__mocks__/react-query/useMutation.ts +++ /dev/null @@ -1,5 +0,0 @@ -const useMutation = jest.fn().mockReturnValue({ - useMutation: jest.fn(), -}); - -export { useMutation }; diff --git a/compose/neurosynth-frontend/src/__mocks__/react-query/useQueryClient.ts b/compose/neurosynth-frontend/src/__mocks__/react-query/useQueryClient.ts deleted file mode 100644 index 53063b50d..000000000 --- a/compose/neurosynth-frontend/src/__mocks__/react-query/useQueryClient.ts +++ /dev/null @@ -1,5 +0,0 @@ -const useQueryClient = jest.fn().mockReturnValue({ - invalidateQueries: jest.fn(), -}); - -export { useQueryClient }; diff --git a/compose/neurosynth-frontend/src/global.styles.ts b/compose/neurosynth-frontend/src/global.styles.ts new file mode 100644 index 000000000..a2a81ea79 --- /dev/null +++ b/compose/neurosynth-frontend/src/global.styles.ts @@ -0,0 +1,20 @@ +import { Style } from 'index'; + +const GlobalStyles: Style = { + colorPulseAnimation: { + animation: 'pulse 2s infinite', + '@keyframes pulse': { + '0%': { + backgroundColor: 'success.light', + }, + '50%': { + backgroundColor: 'white', + }, + '100%': { + backgroundColor: 'success.light', + }, + }, + }, +}; + +export default GlobalStyles; diff --git a/compose/neurosynth-frontend/src/helpers/utils.tsx b/compose/neurosynth-frontend/src/helpers/utils.tsx index 65d488fb5..201a95da8 100644 --- a/compose/neurosynth-frontend/src/helpers/utils.tsx +++ b/compose/neurosynth-frontend/src/helpers/utils.tsx @@ -45,3 +45,22 @@ export const stringToColor = (stringArg: string) => { } return color; }; + +export const stringToNumber = (s: string): { value: number; isValid: boolean } => { + if (s === '') + return { + value: 0, + isValid: false, + }; + const parsedNum = Number(s); + if (isNaN(parsedNum)) { + return { + value: 0, + isValid: false, + }; + } + return { + value: parsedNum, + isValid: true, + }; +}; diff --git a/compose/neurosynth-frontend/src/hooks/external/useGetBibtexCitations.tsx b/compose/neurosynth-frontend/src/hooks/external/useGetBibtexCitations.tsx new file mode 100644 index 000000000..ea90f2446 --- /dev/null +++ b/compose/neurosynth-frontend/src/hooks/external/useGetBibtexCitations.tsx @@ -0,0 +1,100 @@ +import axios, { AxiosError } from 'axios'; +import { stringToNumber } from 'helpers/utils'; +import { ICurationStubStudy } from 'pages/Curation/Curation.types'; +import { useMutation } from 'react-query'; + +const stringAsAuthorArray = (authors: string): IBibtex['author'] => { + const authorsStringToArray = authors.split(', ').map((author) => { + const nameAsArray = author.split(' '); + if (nameAsArray.length === 0) { + return { given: '', family: '' }; + } else if (nameAsArray.length === 1) { + return { given: nameAsArray[0], family: '' }; + } else { + const givenNames = nameAsArray.slice(0, nameAsArray.length - 1).join(' '); + return { given: givenNames, family: nameAsArray[nameAsArray.length - 1] }; + } + }); + return authorsStringToArray; +}; + +const generateBibtexNote = (study: ICurationStubStudy) => { + let bibtexNote = ''; + if (study.pmid) bibtexNote = `PMID: ${study.pmid}`; + if (study.pmcid) bibtexNote = `${bibtexNote}; PMCID: ${study.pmcid}`; + if (study.neurostoreId) bibtexNote = `${bibtexNote}; Neurosynth ID: ${study.neurostoreId}`; + if (study.identificationSource.label) { + bibtexNote = `${bibtexNote}; Source: ${study.identificationSource.label}`; + } + if (study.tags.length > 0) { + const tagString = study.tags.reduce( + (prev, curr, index, arr) => + `${prev}${curr.label}${index === arr.length - 1 ? '' : ','}`, + '' + ); + bibtexNote = `${bibtexNote}; Tags: ${tagString}`; + } + + return bibtexNote; +}; + +// this is not the complete Bibtex type. There are other other types +// as described here: https://bibtex.eu/types/article/ however these are the most significant +export interface IBibtex { + author: { given: string; family: string }[]; + title: string; + DOI: string; + note?: string; + URL: string; + abstract: string; + issued: { + 'date-parts'?: [number, number, number][]; + }; + 'container-title': string; // journal + type: string; // article-journal for papers +} + +// if we do not receive bibtex data from the api, then we create our own with the data we have +export const generateBibtex = (study: ICurationStubStudy): IBibtex => { + const { isValid, value } = stringToNumber(study.articleYear || ''); + + return { + title: study.title, + type: 'article-journal', + DOI: study.doi || '', + URL: study.articleLink || '', + abstract: study.abstractText || '', + note: generateBibtexNote(study), + issued: { + 'date-parts': isValid ? [[value, 0, 0]] : undefined, + }, + 'container-title': study.journal || '', + author: stringAsAuthorArray(study.authors || ''), + }; +}; + +/** + * NOTE: this is a get request but we use useMutation so that we can query the data imperatively. + * This means that there is no smart refetching + * https://github.com/TanStack/query/discussions/3675 + */ + +const useGetBibtexCitations = () => { + return useMutation(async (study) => { + let res: IBibtex; + try { + res = ( + await axios.get<{ message: IBibtex }>( + `https://api.crossref.org/v1/works/${study.doi}` + ) + ).data.message; + } catch (e) { + res = generateBibtex(study); + } + // add a note with relevant neurosynth related data for provenance + res.note = generateBibtexNote(study); + return res; + }); +}; + +export default useGetBibtexCitations; diff --git a/compose/neurosynth-frontend/src/pages/Curation/Curation.helpers.ts b/compose/neurosynth-frontend/src/pages/Curation/Curation.helpers.ts new file mode 100644 index 000000000..fab3861a4 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Curation/Curation.helpers.ts @@ -0,0 +1,9 @@ +export const downloadFile = (filename: string, fileContents: BlobPart, contentType: string) => { + const blob = new Blob([fileContents], { type: contentType }); + const element = window.document.createElement('a'); + element.href = window.URL.createObjectURL(blob); + element.download = filename; + window.document.body.appendChild(element); + element.click(); + window.document.body.removeChild(element); +}; diff --git a/compose/neurosynth-frontend/src/pages/Curation/CurationPage.tsx b/compose/neurosynth-frontend/src/pages/Curation/CurationPage.tsx index 8f20b77da..3b7164446 100644 --- a/compose/neurosynth-frontend/src/pages/Curation/CurationPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Curation/CurationPage.tsx @@ -1,8 +1,13 @@ import SchemaIcon from '@mui/icons-material/Schema'; import { Box, Button } from '@mui/material'; -import PrismaDialog from 'pages/Curation/components/PrismaDialog'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs'; +import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; +import GlobalStyles from 'global.styles'; +import { useGetCurationSummary, useGetStudysetById } from 'hooks'; +import useUserCanEdit from 'hooks/useUserCanEdit'; +import CurationBoard from 'pages/Curation/components/CurationBoard'; +import PrismaDialog from 'pages/Curation/components/PrismaDialog'; import { IProjectPageLocationState } from 'pages/Project/ProjectPage'; import { useInitProjectStoreIfRequired, @@ -14,10 +19,7 @@ import { } from 'pages/Project/store/ProjectStore'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { useGetStudysetById, useGetCurationSummary } from 'hooks'; -import useUserCanEdit from 'hooks/useUserCanEdit'; -import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; -import CurationBoard from 'pages/Curation/components/CurationBoard'; +import CurationDownloadIncludedStudiesButton from './components/CurationDownloadIncludedStudiesButton'; const CurationPage: React.FC = (props) => { const [prismaIsOpen, setPrismaIsOpen] = useState(false); @@ -36,8 +38,11 @@ const CurationPage: React.FC = (props) => { const { included, uncategorized } = useGetCurationSummary(); const { data: studyset } = useGetStudysetById(studysetId || '', false); + const extractionStepInitialized = + studysetId && annotationId && (studyset?.studies?.length || 0) > 0; + const handleMoveToExtractionPhase = () => { - if (studysetId && annotationId && (studyset?.studies?.length || 0) > 0) { + if (extractionStepInitialized) { navigate(`/projects/${projectId}/extraction`); } else { navigate(`/projects/${projectId}`, { @@ -85,6 +90,7 @@ const CurationPage: React.FC = (props) => { + {isPrisma && ( <> { @@ -104,7 +110,7 @@ const CurationPage: React.FC = (props) => { )} diff --git a/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.spec.tsx b/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.spec.tsx new file mode 100644 index 000000000..5a33183f5 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.spec.tsx @@ -0,0 +1,178 @@ +import { act, render, screen } from '@testing-library/react'; +import CurationDownloadIncludedStudiesButton from 'pages/Curation/components/CurationDownloadIncludedStudiesButton'; +import QueryClientTestingWrapper from 'testing/QueryClientTestingWrapper'; +import userEvent from '@testing-library/user-event'; +import { useProjectCurationColumns } from 'pages/Project/store/ProjectStore'; +import { ICurationColumn } from '../Curation.types'; +import { defaultIdentificationSources } from 'pages/Project/store/ProjectStore.types'; +import { downloadFile } from '../Curation.helpers'; +import useGetBibtexCitations, { IBibtex } from 'hooks/external/useGetBibtexCitations'; + +jest.mock('react-query', () => { + return { + ...jest.requireActual('react-query'), + useMutation: jest.fn().mockReturnValue({ + mutateAsync: jest.fn(), + }), + }; +}); +jest.mock('pages/Curation/Curation.helpers.ts', () => { + return { + downloadFile: jest.fn(), + }; +}); +jest.mock('pages/Project/store/ProjectStore'); + +const mockCurationColumns: ICurationColumn[] = [ + { + name: 'excluded', + id: '1', + stubStudies: [], + }, + { + name: 'included', + id: '2', + stubStudies: [ + { + id: 'included_1', + title: 'included_1', + authors: 'John Smith', + pmid: 'included_pmid_1', + pmcid: 'included_pmcid_1', + doi: 'included_doi_1', + articleYear: 'included_articleyear_1', + journal: 'included_journal_1', + articleLink: 'included_articlelink_1', + tags: [], + identificationSource: defaultIdentificationSources.neurostore, + keywords: '', + abstractText: 'included_abstract_1', + exclusionTag: null, + neurostoreId: 'included_neurostoreid_1', + searchTerm: '', + }, + { + id: 'included_2', + title: 'included_2', + authors: 'included_authors_2', + pmid: 'included_pmid_2', + pmcid: 'included_pmcid_2', + doi: '', + articleYear: 'included_articleyear_2', + journal: 'included_journal_2', + articleLink: 'included_articlelink_2', + tags: [ + { + id: 'tag_1', + label: 'tag_1_label', + isExclusionTag: false, + isAssignable: true, + }, + { + id: 'tag_2', + label: 'tag_2_label', + isExclusionTag: false, + isAssignable: true, + }, + ], + identificationSource: defaultIdentificationSources.neurostore, + keywords: '', + abstractText: 'included_abstract_2', + exclusionTag: null, + neurostoreId: 'included_neurostoreid_2', + searchTerm: '', + }, + ], + }, +]; + +describe('CurationDownloadIncludedStudiesButton', () => { + it('should render', () => { + render(, { wrapper: QueryClientTestingWrapper }); + }); + + it('renders the button group and opens the dropdown menu when clicked', () => { + render(, { wrapper: QueryClientTestingWrapper }); + const dropdownButton = screen.getByTestId('ArrowDropDownIcon'); + expect(dropdownButton).toBeInTheDocument(); + expect(screen.getByText('Download INCLUDED as CSV')).toBeInTheDocument(); + userEvent.click(dropdownButton); + expect( + screen.getByRole('menuitem', { name: 'Download INCLUDED as BibTeX' }) + ).toBeInTheDocument(); + }); + + it('downloads CSVs when the download CSV button is clicked', () => { + const csvStudies = + `"Title","Authors","PMID","PMCID","DOI","Year","Journal","Link","Source","Tags","Neurosynth ID","Search Term"\r\n` + + `"included_1","John Smith","included_pmid_1","included_pmcid_1","included_doi_1","included_articleyear_1","included_journal_1","included_articlelink_1","Neurostore","","included_neurostoreid_1",""\r\n` + + `"included_2","included_authors_2","included_pmid_2","included_pmcid_2","","included_articleyear_2","included_journal_2","included_articlelink_2","Neurostore","tag_1_label,tag_2_label","included_neurostoreid_2",""`; + + (useProjectCurationColumns as jest.Mock).mockReturnValue(mockCurationColumns); + + render(, { wrapper: QueryClientTestingWrapper }); + userEvent.click(screen.getByText('Download INCLUDED as CSV')); + expect(downloadFile).toHaveBeenCalledTimes(1); + expect(downloadFile).toHaveBeenCalledWith( + `project-name:Curation:${new Date().toLocaleDateString()}.csv`, + csvStudies, + 'text/csv;charset=utf-8' + ); + }); + + it('downloads BibTeX citations when the download BibTeX button is clicked', async () => { + const mutateAsyncMock = useGetBibtexCitations().mutateAsync as jest.Mock; + mutateAsyncMock.mockReturnValue({ + author: [{ given: 'John', family: 'Smith' }], + title: 'included_1', + DOI: 'included_doi_1', + note: '', + URL: 'included_articlelink_1', + abstract: 'included_abstract_1', + issued: [{ 'date-parts': [2020, 0, 0] }], + 'container-title': 'included_journal_1', + type: 'article-journal', + } as IBibtex); + + const expectedBibtex = + `@article{Smith2020included_1,\n` + + `\tauthor = {Smith, John},\n` + + `\tjournal = {included\\textunderscore{}journal\\textunderscore{}1},\n` + + `\tdoi = {included_doi_1},\n` + + `\tyear = {2020},\n` + + `\tnote = {},\n` + + `\ttitle = {included\\textunderscore{}1},\n` + + `\turl = {included_articlelink_1},\n` + + `\thowpublished = {included\\textunderscore{}articlelink\\textunderscore{}1},\n` + + `}\n` + + `@article{included_2,\n` + + `\tauthor = {, included\\textunderscore{}authors\\textunderscore{}2},\n` + + `\tjournal = {included\\textunderscore{}journal\\textunderscore{}2},\n` + + `\tdoi = {},\n` + + `\tnote = {PMID: included\\textunderscore{}pmid\\textunderscore{}2; PMCID: included\\textunderscore{}pmcid\\textunderscore{}2; Neurosynth ID: included\\textunderscore{}neurostoreid\\textunderscore{}2; Source: Neurostore; Tags: tag\\textunderscore{}1\\textunderscore{}label,tag\\textunderscore{}2\\textunderscore{}label},\n` + + `\ttitle = {included\\textunderscore{}2},\n` + + `\turl = {included_articlelink_2},\n` + + `\thowpublished = {included\\textunderscore{}articlelink\\textunderscore{}2},\n` + + `}\n\n`; + + (useProjectCurationColumns as jest.Mock).mockReturnValue(mockCurationColumns); + + await act(async () => { + render(, { + wrapper: QueryClientTestingWrapper, + }); + const dropdownButton = screen.getByTestId('ArrowDropDownIcon'); + expect(dropdownButton).toBeInTheDocument(); + userEvent.click(dropdownButton); + }); + await act(async () => { + userEvent.click(screen.getByText('Download INCLUDED as BibTeX')); + }); + expect(mutateAsyncMock).toHaveBeenCalledWith(mockCurationColumns[1].stubStudies[0]); // second should not be called as it does not have DOI + expect(downloadFile).toHaveBeenCalledWith( + `project-name:Curation:${new Date().toLocaleDateString()}.bib`, + expectedBibtex, + 'text/plain' + ); + }); +}); diff --git a/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.tsx b/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.tsx new file mode 100644 index 000000000..aebc17b43 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Curation/components/CurationDownloadIncludedStudiesButton.tsx @@ -0,0 +1,147 @@ +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { Box, Button, ButtonGroup, MenuItem, MenuList } from '@mui/material'; +import NeurosynthPopper from 'components/NeurosynthPopper/NeurosynthPopper'; +import ProgressLoader from 'components/ProgressLoader'; +import useGetBibtexCitations, { + generateBibtex, + IBibtex, +} from 'hooks/external/useGetBibtexCitations'; +import { useProjectCurationColumns, useProjectName } from 'pages/Project/store/ProjectStore'; +import { executeHTTPRequestsAsBatches } from 'pages/SleuthImport/SleuthImport.helpers'; +import { useRef, useState } from 'react'; +import { downloadFile } from '../Curation.helpers'; +import { ICurationStubStudy } from 'pages/Curation/Curation.types'; +const { Cite } = require('@citation-js/core'); +require('@citation-js/plugin-bibtex'); +require('@citation-js/plugin-doi'); + +const CurationDownloadIncludedStudiesButton: React.FC = () => { + const { mutateAsync } = useGetBibtexCitations(); + const [optionsIsOpen, setOptionsIsOpen] = useState(false); + const anchorRef = useRef(null); + const includedStudies = useProjectCurationColumns(); + const projectName = useProjectName(); + const [isLoading, setIsLoading] = useState(false); + + const retrieveBibtex = async (includedStudies: ICurationStubStudy[]) => { + const responses = await executeHTTPRequestsAsBatches( + includedStudies, + (study) => { + if (!study.doi) { + return new Promise((res) => { + const fakeResponse: IBibtex = { + ...generateBibtex(study), + }; + res(fakeResponse); + }); + } else { + return mutateAsync(study); + } + }, + 30 + ); + const citeObj = new Cite(responses); + return citeObj.format('bibtex', { format: 'text' }) as string; + }; + + const retrieveCSV = (studies: ICurationStubStudy[]) => { + const mappedCSVStudyObjs = studies.map((study) => ({ + title: study.title || '', + authors: study.authors || '', + pmid: study.pmid || '', + pmcid: study.pmcid || '', + doi: study.doi || '', + articleYear: study.articleYear || '', + journal: study.journal || '', + articleLink: study.articleLink || '', + source: study.identificationSource.label || '', + tags: study.tags.reduce( + (prev, curr, index, arr) => + `${prev}${curr.label}${index === arr.length - 1 ? '' : ','}`, + '' + ), + neurostoreId: study.neurostoreId || '', + searchTerm: study.searchTerm || '', + })); + + return [ + { + title: 'Title', + authors: 'Authors', + pmid: 'PMID', + pmcid: 'PMCID', + doi: 'DOI', + articleYear: 'Year', + journal: 'Journal', + articleLink: 'Link', + source: 'Source', + tags: 'Tags', + neurostoreId: 'Neurosynth ID', + searchTerm: 'Search Term', + }, + ...mappedCSVStudyObjs, + ] + .map((study) => { + const studyValues = Object.values(study); // order is respected + return studyValues + .map(String) + .map((value) => value.replaceAll('"', '""')) + .map((value) => `"${value}"`) + .join(','); + }) + .join('\r\n'); + }; + + const handleDownloadIncludedStudies = async (format: 'bibtex' | 'csv') => { + setIsLoading(true); + const allIncludedStudies = includedStudies[includedStudies.length - 1]; + const date = new Date().toLocaleDateString(); + if (format === 'bibtex') { + const bibtexStudies = await retrieveBibtex(allIncludedStudies.stubStudies); + downloadFile(`${projectName}:Curation:${date}.bib`, bibtexStudies, 'text/plain'); + } else { + const csvStudies = retrieveCSV(allIncludedStudies.stubStudies); + downloadFile( + `${projectName}:Curation:${date}.csv`, + csvStudies, + 'text/csv;charset=utf-8' + ); + } + setIsLoading(false); + }; + + return ( + <> + setOptionsIsOpen(false)} + anchorElement={anchorRef.current} + open={optionsIsOpen} + > + + + handleDownloadIncludedStudies('bibtex')} + value="PNG" + > + Download INCLUDED as BibTeX + + + + + + + + + + ); +}; + +export default CurationDownloadIncludedStudiesButton; diff --git a/compose/neurosynth-frontend/src/pages/CurationImport/components/CurationImportStandardFormatUpload.tsx b/compose/neurosynth-frontend/src/pages/CurationImport/components/CurationImportStandardFormatUpload.tsx index 1f5ef10b3..d685c0f9a 100644 --- a/compose/neurosynth-frontend/src/pages/CurationImport/components/CurationImportStandardFormatUpload.tsx +++ b/compose/neurosynth-frontend/src/pages/CurationImport/components/CurationImportStandardFormatUpload.tsx @@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid'; import CurationImportBaseStyles from 'pages/CurationImport/components/CurationImport.styles'; import { ICurationStubStudy } from 'pages/Curation/Curation.types'; import CurationPopupIdentificationSourceSelector from 'pages/Curation/components/CurationPopupIdentificationSourceSelector'; -const Cite = require('citation-js'); +const { Cite } = require('@citation-js/core'); require('@citation-js/plugin-enw'); require('@citation-js/plugin-bibtex'); require('@citation-js/plugin-ris'); diff --git a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx index 94eaf7e94..10901dfdb 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx @@ -2,8 +2,6 @@ import BookmarkIcon from '@mui/icons-material/Bookmark'; import CheckIcon from '@mui/icons-material/Check'; import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; import { Box, Button, Chip, Typography } from '@mui/material'; -import ExtractionOutOfSync from 'pages/Extraction/components/ExtractionOutOfSync'; -import ReadOnlyStudySummaryVirtualizedItem from 'pages/Extraction/components/ReadOnlyStudySummary'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs'; import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; @@ -13,21 +11,23 @@ import useGetExtractionSummary from 'hooks/useGetExtractionSummary'; import useGetWindowHeight from 'hooks/useGetWindowHeight'; import useUserCanEdit from 'hooks/useUserCanEdit'; import { StudyReturn } from 'neurostore-typescript-sdk'; +import ExtractionOutOfSync from 'pages/Extraction/components/ExtractionOutOfSync'; +import ReadOnlyStudySummaryVirtualizedItem from 'pages/Extraction/components/ReadOnlyStudySummary'; +import { resolveStudysetAndCurationDifferences } from 'pages/Extraction/Extraction.helpers'; import { IProjectPageLocationState } from 'pages/Project/ProjectPage'; import { + useGetProjectIsLoading, useInitProjectStoreIfRequired, - useProjectCurationColumn, + useProjectCurationColumns, useProjectExtractionStudyStatusList, useProjectExtractionStudysetId, useProjectMetaAnalysisCanEdit, useProjectName, - useProjectNumCurationColumns, useProjectUser, } from 'pages/Project/store/ProjectStore'; import { useEffect, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; -import { resolveStudysetAndCurationDifferences } from 'pages/Extraction/Extraction.helpers'; export enum EExtractionStatus { 'COMPLETED' = 'completed', @@ -66,8 +66,8 @@ const ExtractionPage: React.FC = (props) => { const projectName = useProjectName(); const studysetId = useProjectExtractionStudysetId(); const studyStatusList = useProjectExtractionStudyStatusList(); - const numColumns = useProjectNumCurationColumns(); - const curationIncludedStudies = useProjectCurationColumn(numColumns - 1); + const columns = useProjectCurationColumns(); + const loading = useGetProjectIsLoading(); const extractionSummary = useGetExtractionSummary(projectId || ''); const canEditMetaAnalyses = useProjectMetaAnalysisCanEdit(); const projectUser = useProjectUser(); @@ -99,18 +99,15 @@ const ExtractionPage: React.FC = (props) => { const [showReconcilePrompt, setShowReconcilePrompt] = useState(false); useEffect(() => { - if ( - !getStudysetIsLoading && - (curationIncludedStudies?.stubStudies?.length || 0) > 0 && - studyset?.studies - ) { + if (!loading && !getStudysetIsLoading && columns.length > 0 && studyset?.studies) { + const includedStudies = columns[columns.length - 1].stubStudies; const isDifferent = resolveStudysetAndCurationDifferences( - curationIncludedStudies.stubStudies, + includedStudies, studyset.studies as StudyReturn[] ); setShowReconcilePrompt(isDifferent); } - }, [curationIncludedStudies, getStudysetIsLoading, studyset?.studies]); + }, [columns, getStudysetIsLoading, studyset?.studies, loading]); useEffect(() => { if (studyStatusList && studyset?.studies) { diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionOutOfSync.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionOutOfSync.tsx index fb7f38320..93f3a945b 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionOutOfSync.tsx +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionOutOfSync.tsx @@ -6,6 +6,7 @@ import useIngest from 'hooks/studies/useIngest'; import { BaseStudy, BaseStudyReturn, StudyReturn } from 'neurostore-typescript-sdk'; import { useSnackbar } from 'notistack'; import { + useAllowEditMetaAnalyses, useProjectCurationColumn, useProjectExtractionAnnotationId, useProjectExtractionStudysetId, @@ -22,6 +23,7 @@ const ExtractionOutOfSync: React.FC = (props) => { const annotationId = useProjectExtractionAnnotationId(); const { data: studyset } = useGetStudysetById(studysetId, true); // set this to true as it is already cached in extractionPage const numColumns = useProjectNumCurationColumns(); + const setAllowEditMetaAnalyses = useAllowEditMetaAnalyses(); const curationIncludedStudies = useProjectCurationColumn(numColumns - 1); const { mutateAsync: ingest } = useIngest(); const { mutateAsync: updateStudyset } = useUpdateStudyset(); @@ -84,7 +86,7 @@ const ExtractionOutOfSync: React.FC = (props) => { }); const selectedVersions = selectBestVersionsForStudyset(newBaseStudiesToAdd); - await updateStudyset({ + const updatedStudyset = await updateStudyset({ studysetId: studysetId, studyset: { studies: [...existingStudies, ...selectedVersions], @@ -96,6 +98,10 @@ const ExtractionOutOfSync: React.FC = (props) => { await setAnalysesInAnnotationAsIncluded(annotationId); enqueueSnackbar('synced curation and studyset successfully', { variant: 'success' }); + + if (updatedStudyset.data.studies && updatedStudyset.data.studies.length === 0) { + setAllowEditMetaAnalyses(false); + } } catch (e) { console.error(e); } finally { diff --git a/compose/neurosynth-frontend/src/pages/Project/components/ProjectSpecificationStep.tsx b/compose/neurosynth-frontend/src/pages/Project/components/ProjectSpecificationStep.tsx index c5318ee88..7a69f7ec2 100644 --- a/compose/neurosynth-frontend/src/pages/Project/components/ProjectSpecificationStep.tsx +++ b/compose/neurosynth-frontend/src/pages/Project/components/ProjectSpecificationStep.tsx @@ -31,7 +31,7 @@ const ProjectSpecificationStep: React.FC = (prop }, [disabled, location?.state]); const handleClickProceed = () => { - allowEditMetaAnalyses(); + allowEditMetaAnalyses(true); navigate(`/projects/${projectId}/meta-analyses`); }; diff --git a/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.ts b/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.ts index 9da8f2489..1c398bd84 100644 --- a/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.ts +++ b/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.ts @@ -370,14 +370,14 @@ const useProjectStore = create()((set, get) => { get().updateProjectInDBDebounced(); }, - allowEditMetaAnalyses: () => { + allowEditMetaAnalyses: (allowed: boolean) => { set((state) => ({ ...state, provenance: { ...state.provenance, metaAnalysisMetadata: { ...state.provenance.metaAnalysisMetadata, - canEditMetaAnalyses: true, + canEditMetaAnalyses: allowed, }, }, })); diff --git a/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.types.ts b/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.types.ts index 550b78d6f..679aaacdc 100644 --- a/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.types.ts +++ b/compose/neurosynth-frontend/src/pages/Project/store/ProjectStore.types.ts @@ -159,7 +159,7 @@ export type ProjectStoreActions = { replaceStudyListStatusId: (idToFindAndReplace: string, replaceWithId: string) => void; setGivenStudyStatusesAsComplete: (studyIdList: string[]) => void; deleteStub: (columnIndex: number, stubId: string) => void; - allowEditMetaAnalyses: () => void; + allowEditMetaAnalyses: (allowEditMetaAnalysis: boolean) => void; }; export type TProjectStore = INeurosynthProjectReturn & diff --git a/compose/neurosynth-frontend/src/pages/Project/store/__mocks__/ProjectStore.ts b/compose/neurosynth-frontend/src/pages/Project/store/__mocks__/ProjectStore.ts index 3e4af320c..2a4bfa311 100644 --- a/compose/neurosynth-frontend/src/pages/Project/store/__mocks__/ProjectStore.ts +++ b/compose/neurosynth-frontend/src/pages/Project/store/__mocks__/ProjectStore.ts @@ -14,6 +14,10 @@ const useProjectExtractionAddOrUpdateStudyListStatus = jest.fn().mockReturnValue const useProjectUser = jest.fn().mockReturnValue('user-id'); +const useProjectName = jest.fn().mockReturnValue('project-name'); + +const useProjectCurationColumns = jest.fn(); + export { useProjectExtractionAnnotationId, useProjectExtractionStudysetId, @@ -23,4 +27,6 @@ export { useProjectMetaAnalysisCanEdit, useProjectExtractionAddOrUpdateStudyListStatus, useProjectUser, + useProjectName, + useProjectCurationColumns, }; diff --git a/compose/neurosynth-frontend/src/pages/Projects/components/ProjectsPageCard.tsx b/compose/neurosynth-frontend/src/pages/Projects/components/ProjectsPageCard.tsx index 007baa9df..1d32ca744 100644 --- a/compose/neurosynth-frontend/src/pages/Projects/components/ProjectsPageCard.tsx +++ b/compose/neurosynth-frontend/src/pages/Projects/components/ProjectsPageCard.tsx @@ -12,6 +12,7 @@ import ProjectsPageCardSummaryMetaAnalyses from './ProjectsPageCardSummaryMetaAn import { MetaAnalysis } from 'neurosynth-compose-typescript-sdk'; import LockIcon from '@mui/icons-material/Lock'; import PublicIcon from '@mui/icons-material/Public'; +import ChangeHistoryIcon from '@mui/icons-material/ChangeHistory'; const isToday = (date: Date) => { const today = new Date(); @@ -174,6 +175,15 @@ const ProjectsPageCard: React.FC = (props) => { sx={{ mr: '6px' }} /> )} + {provenance?.curationMetadata?.prismaConfig?.isPrisma && ( + } + variant="outlined" + size="small" + sx={{ mr: '6px', pl: '2px' }} + /> + )} {studyset && ( { return splitStr.trim(); }; -export const stringToNumber = (s: string): { value: number; isValid: boolean } => { - const parsedNum = Number(s); - if (isNaN(parsedNum)) { - return { - value: 0, - isValid: false, - }; - } - return { - value: parsedNum, - isValid: true, - }; -}; - // [x, y, z] export const parseCoordinate = (coordinates: string): { coords: number[]; isValid: boolean } => { const parsedCoordinates: number[] = []; @@ -458,7 +445,7 @@ export const organizeSleuthStubsIntoHTTPRequests = ( export const executeHTTPRequestsAsBatches = async ( requestList: T[], - mapFunc: (request: T) => Promise>, + mapFunc: (request: T) => Promise, rateLimit: number, delayInMS?: number, progressCallbackFunc?: (progress: number) => void @@ -468,7 +455,7 @@ export const executeHTTPRequestsAsBatches = async ( arrayOfRequestArrays.push(requestList.slice(i, i + rateLimit)); } - const batchedResList: AxiosResponse[] = []; + const batchedResList: Y[] = []; for (const requests of arrayOfRequestArrays) { /** * I have to do the mapping from object to HTTP request here because @@ -583,10 +570,6 @@ export const lookForPMIDsAndFetchStudyDetails = async ( count: '1', idlist: [baseStudy.pmid], }, - header: { - type: 'NEUROSTORE_MOCK', - version: 'NA', - }, }, status: 200, statusText: 'OK', diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.styles.ts b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.styles.ts index a58bceab5..f44bf10e1 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.styles.ts +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.styles.ts @@ -30,20 +30,6 @@ const EditStudyToolbarStyles: Style = { width: '42px', height: '42px', }, - colorPulseAnimation: { - animation: 'pulse 2s infinite', - '@keyframes pulse': { - '0%': { - backgroundColor: 'success.light', - }, - '50%': { - backgroundColor: 'white', - }, - '100%': { - backgroundColor: 'success.light', - }, - }, - }, }; export default EditStudyToolbarStyles; diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx index a832184bf..d069ac657 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx @@ -22,6 +22,7 @@ import { useNavigate } from 'react-router-dom'; import EditStudyToolbarStyles from './EditStudyToolbar.styles'; import { IProjectPageLocationState } from 'pages/Project/ProjectPage'; import { useUserCanEdit } from 'hooks'; +import GlobalStyles from 'global.styles'; const getCurrSelectedChipText = (selectedChip: EExtractionStatus) => { switch (selectedChip) { @@ -216,7 +217,7 @@ const EditStudyToolbar: React.FC<{ isViewOnly?: boolean }> = ({ isViewOnly = fal diff --git a/compose/neurosynth-frontend/src/setupTests.ts b/compose/neurosynth-frontend/src/setupTests.ts index d3723dac7..c5c85429e 100644 --- a/compose/neurosynth-frontend/src/setupTests.ts +++ b/compose/neurosynth-frontend/src/setupTests.ts @@ -10,3 +10,4 @@ import '@testing-library/jest-dom'; import 'regenerator-runtime/runtime'; import React from 'react'; global.React = React; +global.URL.createObjectURL = jest.fn(); diff --git a/compose/neurosynth-frontend/src/testing/QueryClientTestingWrapper.tsx b/compose/neurosynth-frontend/src/testing/QueryClientTestingWrapper.tsx new file mode 100644 index 000000000..d83b3565f --- /dev/null +++ b/compose/neurosynth-frontend/src/testing/QueryClientTestingWrapper.tsx @@ -0,0 +1,8 @@ +import { QueryClient, QueryClientProvider } from 'react-query'; + +const mockQueryClient = new QueryClient(); +const QueryClientTestingWrapper: React.FC = ({ children }) => { + return {children}; +}; + +export default QueryClientTestingWrapper;