Skip to content

Commit

Permalink
766 add feature to download list of included papers as csv (#796)
Browse files Browse the repository at this point in the history
* feat: added download button and logic to convert to bibtex

* feat: added feature to download curation CSVs and bibtex files

* feat: added window

* feat: added testing

* fix: remove unused imports

* fix: failing cypress tests
  • Loading branch information
nicoalee committed Jul 29, 2024
1 parent 51440f8 commit 80ab474
Show file tree
Hide file tree
Showing 27 changed files with 615 additions and 321 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions compose/neurosynth-frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ module.exports = {
roots: ['<rootDir>'],
modulePaths: ['<rootDir>'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
};
288 changes: 53 additions & 235 deletions compose/neurosynth-frontend/package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions compose/neurosynth-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

20 changes: 20 additions & 0 deletions compose/neurosynth-frontend/src/global.styles.ts
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions compose/neurosynth-frontend/src/helpers/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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<IBibtex, AxiosError, ICurationStubStudy, unknown>(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;
Original file line number Diff line number Diff line change
@@ -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);
};
37 changes: 26 additions & 11 deletions compose/neurosynth-frontend/src/pages/Curation/CurationPage.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
Expand All @@ -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}`, {
Expand Down Expand Up @@ -85,6 +90,7 @@ const CurationPage: React.FC = (props) => {
<ProjectIsLoadingText />
</Box>
<Box sx={{ marginRight: '1rem' }}>
<CurationDownloadIncludedStudiesButton />
{isPrisma && (
<>
<PrismaDialog
Expand All @@ -94,8 +100,8 @@ const CurationPage: React.FC = (props) => {
<Button
onClick={() => setPrismaIsOpen(true)}
variant="outlined"
sx={{ marginRight: '1rem', width: '234px' }}
endIcon={<SchemaIcon />}
sx={{ marginLeft: '0.5rem', width: '180px' }}
startIcon={<SchemaIcon />}
>
PRISMA diagram
</Button>
Expand All @@ -104,7 +110,7 @@ const CurationPage: React.FC = (props) => {
<Button
variant="contained"
disableElevation
sx={{ marginRight: '1rem', width: '234px' }}
sx={{ marginLeft: '0.5rem', width: '180px' }}
onClick={() => navigate(`/projects/${projectId}/curation/import`)}
disabled={!canEdit}
>
Expand All @@ -115,11 +121,20 @@ const CurationPage: React.FC = (props) => {
onClick={handleMoveToExtractionPhase}
variant="contained"
color="success"
sx={{ width: '234px' }}
sx={{
width: '180px',
ml: '0.5rem',
...(extractionStepInitialized
? { color: 'white' }
: {
...GlobalStyles.colorPulseAnimation,
color: 'success.dark',
}),
}}
disableElevation
disabled={!canEdit}
>
Move To Extraction Phase
{extractionStepInitialized ? 'view extraction' : 'go to extraction'}
</Button>
)}
</Box>
Expand Down
Loading

0 comments on commit 80ab474

Please sign in to comment.