diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx index 7c2b320d..bd1a7dfe 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx @@ -7,7 +7,7 @@ const PAGE_NAME = 'BaseStudyPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', `https://api.semanticscholar.org/**`, { fixture: 'semanticScholar', }).as('semanticScholarFixture'); diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/EditStudyPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/EditStudyPage.cy.tsx index cbe97e57..586d7573 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/EditStudyPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/EditStudyPage.cy.tsx @@ -7,7 +7,7 @@ const PAGE_NAME = 'EditStudyPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `https://api.semanticscholar.org/**`, { fixture: 'semanticScholar', diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/LandingPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/LandingPage.cy.tsx index 06a083d0..cd6307ab 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/LandingPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/LandingPage.cy.tsx @@ -6,7 +6,7 @@ const PAGE_NAME = 'LandingPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/base-studies/**`, { fixture: 'baseStudies/baseStudiesNoResults', diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/MetaAnalysisPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/MetaAnalysisPage.cy.tsx index 4b0f70bd..7c045940 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/MetaAnalysisPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/MetaAnalysisPage.cy.tsx @@ -7,7 +7,7 @@ const PAGE_NAME = 'MetaAnalysisPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/specifications/**`, { fixture: 'specification' }).as( 'specificationFixture' diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/ProjectMetaAnalysesPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/ProjectMetaAnalysesPage.cy.tsx index e9cdb41b..a84f002d 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/ProjectMetaAnalysesPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/ProjectMetaAnalysesPage.cy.tsx @@ -7,7 +7,7 @@ const PAGE_NAME = 'ProjectMetaAnalysesPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/specifications/**`, { fixture: 'specification' }).as( 'specificationFixture' diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/ProjectPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/ProjectPage.cy.tsx index ebd266a0..3631c434 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/ProjectPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/ProjectPage.cy.tsx @@ -7,7 +7,7 @@ const PAGE_NAME = 'ProjectPage'; describe(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('POST', `https://www.google-analytics.com/*/**`, {}).as( 'googleAnalyticsFixture' diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/PublicStudiesPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/PublicStudiesPage.cy.tsx index 0cf70328..dc4a824d 100644 --- a/compose/neurosynth-frontend/cypress/e2e/pages/PublicStudiesPage.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/pages/PublicStudiesPage.cy.tsx @@ -9,7 +9,7 @@ const PAGE_NAME = 'StudiesPage'; describe.skip(PAGE_NAME, () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); }); diff --git a/compose/neurosynth-frontend/cypress/e2e/workflows/Curation/ImportStudiesDialog.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/workflows/Curation/ImportStudiesDialog.cy.tsx index c7dfde86..607dfadf 100644 --- a/compose/neurosynth-frontend/cypress/e2e/workflows/Curation/ImportStudiesDialog.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/Curation/ImportStudiesDialog.cy.tsx @@ -2,7 +2,7 @@ describe('ImportStudiesDialog', () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/projects/*`, { fixture: 'projects/projectExtractionStep', @@ -198,14 +198,14 @@ describe('ImportStudiesDialog', () => { }); it('should set the source and show the input', () => { - cy.get('input[role="combobox"').click(); + cy.get('input[role="combobox"]').click(); cy.contains('li', 'Scopus').click(); cy.get('textarea').should('be.visible'); cy.contains(/Input is empty/).should('be.visible'); }); it('should set the sources and enable the next button', () => { - cy.get('input[role="combobox"').click(); + cy.get('input[role="combobox"]').click(); cy.contains('li', 'Scopus').click(); cy.get('textarea[placeholder="paste in valid endnote, bibtex, or RIS syntax"]') .click() @@ -220,7 +220,7 @@ describe('ImportStudiesDialog', () => { }); it('should show an error message', () => { - cy.get('input[role="combobox"').click(); + cy.get('input[role="combobox"]').click(); cy.contains('li', 'Scopus').click(); cy.get('textarea[placeholder="paste in valid endnote, bibtex, or RIS syntax"]').type( 'INVALID FORMAT' @@ -229,7 +229,7 @@ describe('ImportStudiesDialog', () => { }); it('should import studies', () => { - cy.get('input[role="combobox"').click(); + cy.get('input[role="combobox"]').click(); cy.contains('li', 'Scopus').click(); cy.get('textarea[placeholder="paste in valid endnote, bibtex, or RIS syntax"]') .click() @@ -249,7 +249,7 @@ describe('ImportStudiesDialog', () => { }); it('should upload a onenote (ENW) file', () => { - cy.get('input[role="combobox"').click(); + cy.get('input[role="combobox"]').click(); cy.contains('li', 'Scopus').click(); cy.get('label[role="button"]').selectFile( 'cypress/fixtures/standardFiles/onenoteStudies.txt' diff --git a/compose/neurosynth-frontend/cypress/e2e/workflows/Extraction/ExtractionTable.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/workflows/Extraction/ExtractionTable.cy.tsx new file mode 100644 index 00000000..9a75dd51 --- /dev/null +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/Extraction/ExtractionTable.cy.tsx @@ -0,0 +1,693 @@ +/// + +import { INeurosynthProjectReturn } from 'hooks/projects/useGetProjects'; +import { StudyReturn, StudysetReturn } from 'neurostore-typescript-sdk'; +import { IExtractionTableStudy } from 'pages/Extraction/components/ExtractionTable'; + +describe('ExtractionTable', () => { + beforeEach(() => { + cy.clearLocalStorage(); + cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); + cy.intercept('GET', `**/api/projects/*`, { + fixture: 'Extraction/project', + }).as('projectFixture'); + cy.intercept('GET', `**/api/studysets/*`, { fixture: 'studyset' }).as('studysetFixture'); + + cy.intercept('PUT', `**/api/projects/*`, { fixture: 'Extraction/project' }).as( + 'updateProjectFixture' + ); + + cy.intercept('GET', `**/api/studies/*`, { fixture: 'study' }).as('studyFixture'); + cy.intercept('GET', `**/api/annotations/*`, { fixture: 'annotation' }).as( + 'annotationsFixture' + ); + + cy.intercept('GET', `https://api.semanticscholar.org/**`, { + fixture: 'semanticScholar', + }).as('semanticScholarFixture'); + }); + + describe('Filtering', () => { + beforeEach(() => { + cy.login('mocked').visit(`/projects/abc123/extraction`); + }); + + it('should filter the table by year', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(0).click(); + cy.get(`input`) + .eq(0) + .type(studysetStudies[0].year?.toString() || ''); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should filter the table by name', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(1).click(); + cy.get(`input`) + .eq(1) + .type(studysetStudies[0].name || ''); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should filter the table by author', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(2).click(); + cy.get(`input`) + .eq(2) + .type(studysetStudies[0].authors || ''); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should show available journals as options', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + const uniqueJouranls = new Set(); + studysetStudies.forEach((study) => { + if (study.publication) uniqueJouranls.add(study.publication); + }); + cy.get('input').eq(3).click(); + cy.get('div[role="presentation"]').within(() => { + cy.get('li').should('have.length', uniqueJouranls.size); + }); + }); + }); + + it('should filter the table by journal', () => { + cy.get('input').eq(3).click(); + cy.get('div[role="presentation"]').within((menu) => { + cy.get('li').eq(0).click(); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should filter the table by doi', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(4).click(); + cy.get(`input`) + .eq(4) + .type(studysetStudies[0].doi || ''); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should filter the table by pmid', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(5).click(); + cy.get(`input`) + .eq(5) + .type(studysetStudies[0].pmid || ''); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should filter the table by status', () => { + cy.get('div[role="combobox"]').eq(0).click(); + cy.get('div[role="presentation"]').within(() => { + // set to completed + cy.get('li').eq(3).click(); + }); + + cy.get('tbody > tr').should('have.length', 1); + }); + + it('should show filtering chips at the bottom if one or more filters are applied', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(0).click(); + cy.get(`input`) + .eq(0) + .type(studysetStudies[0].year?.toString() || ''); + cy.get('input').eq(1).click(); + cy.get('input') + .eq(1) + .type(studysetStudies[0].name?.toString() || ''); + + cy.contains(`Filtering YEAR: ${studysetStudies[0].year?.toString()}`).should( + 'exist' + ); + cy.contains(`Filtering NAME: ${studysetStudies[0].name?.toString()}`).should( + 'exist' + ); + }); + }); + + it('should remove the filter if the delete button is clicked', () => { + // ARRANGE + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture?.response?.body as StudysetReturn; + const studysetStudies = studyset.studies as StudyReturn[]; + cy.get('input').eq(0).click(); + cy.get(`input`) + .eq(0) + .type(studysetStudies[0].year?.toString() || ''); + }); + cy.get('tbody > tr').should('have.length', 1); + cy.get('[data-testid="CancelIcon"]').should('exist'); + // ACT + cy.get('[data-testid="CancelIcon"]').click(); + + // ASSERT + cy.get('tbody > tr').should('have.length', 3); + cy.get(`[data-testid="CancelIcon"]`).should('not.exist'); + }); + }); + + describe('status', () => { + beforeEach(() => { + cy.login('mocked').visit(`/projects/abc123/extraction`); + }); + + it('should change the study status', () => { + // ARRANGE + cy.get('tbody > tr').eq(0).get('td').eq(6).as('getFirstRowStudyStatusCol'); + cy.get('@getFirstRowStudyStatusCol').within(() => { + cy.get('button').eq(0).should('have.class', 'MuiButton-contained'); + }); + + // ACT + cy.get('@getFirstRowStudyStatusCol').within(() => { + cy.get('button').eq(1).click(); + }); + + // ASSERT + cy.get('@getFirstRowStudyStatusCol').within(() => { + cy.get('button').eq(0).should('have.class', 'MuiButton-outlined'); + cy.get('button').eq(1).should('have.class', 'MuiButton-contained'); + }); + }); + }); + + describe('sorting', () => { + beforeEach(() => { + cy.login('mocked').visit(`/projects/abc123/extraction`); + }); + + it('should sort by year desc', () => { + cy.contains('Year').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort( + (a, b) => (b.year as number) - (a.year as number) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(0) + .should('have.text', sortedStudies[index].year?.toString()); + }); + }); + }); + }); + + it('should sort by year asc', () => { + cy.contains('Year').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort( + (a, b) => (a.year as number) - (b.year as number) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(0) + .should('have.text', sortedStudies[index].year?.toString()); + }); + }); + }); + }); + + it('should sort by name asc', () => { + cy.contains('Name').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (b.name as string).localeCompare(a.name as string) + ); + + console.log(sortedStudies); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(1).should('have.text', sortedStudies[index].name); + }); + }); + }); + }); + + it('should sort by name desc', () => { + cy.contains('Name').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (a.name as string).localeCompare(b.name as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(1).should('have.text', sortedStudies[index].name); + }); + }); + }); + }); + + it('should sort by authors desc', () => { + cy.contains('Authors').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (b.authors as string).localeCompare(a.authors as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(2).should('have.text', sortedStudies[index].authors); + }); + }); + }); + }); + + it('should sort by authors asc', () => { + cy.contains('Authors').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (a.authors as string).localeCompare(b.authors as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(2).should('have.text', sortedStudies[index].authors); + }); + }); + }); + }); + + it('should sort by journal desc', () => { + cy.contains('Journal').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (b.publication as string).localeCompare(a.publication as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(3).should('have.text', sortedStudies[index].publication); + }); + }); + }); + }); + + it('should sort by journal desc', () => { + cy.contains('Journal').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (a.publication as string).localeCompare(b.publication as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(3).should('have.text', sortedStudies[index].publication); + }); + }); + }); + }); + + it('should sort by doi desc', () => { + cy.contains('DOI').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (b.doi as string).localeCompare(a.doi as string) + ); + + console.log(sortedStudies); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(4).should('have.text', sortedStudies[index].doi); + }); + }); + }); + }); + it('should sort by doi asc', () => { + cy.contains('DOI').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (a.doi as string).localeCompare(b.doi as string) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td').eq(4).should('have.text', sortedStudies[index].doi); + }); + }); + }); + }); + + it('should sort by pmid desc', () => { + cy.contains('PMID').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (b?.pmid || '').localeCompare(a?.pmid || '', undefined, { + numeric: true, + }) + ); + + console.log(sortedStudies); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(5) + .should('have.text', sortedStudies[index].pmid ?? ''); + }); + }); + }); + }); + + it('should sort by pmid asc', () => { + cy.contains('PMID').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies = studies.sort((a, b) => + (a?.pmid || '').localeCompare(b?.pmid || '', undefined, { + numeric: true, + }) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(5) + .should('have.text', sortedStudies[index].pmid ?? ''); + }); + }); + }); + }); + + it('should sort by status desc', () => { + cy.contains('Status').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').should('exist'); + + cy.wait('@projectFixture').then((projectFixture) => { + const project = projectFixture?.response?.body as INeurosynthProjectReturn; + const studyStatusList = project.provenance.extractionMetadata.studyStatusList; + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies: IExtractionTableStudy[] = studies + .map((study) => ({ + ...study, + status: studyStatusList.find((status) => status.id === study.id) + ?.status, + })) + .sort((a, b) => + (a?.status || '').localeCompare(b?.status || '', undefined, { + numeric: true, + }) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(6) + .within(() => { + const studyStatus = sortedStudies[index].status; + const buttonIndex = + studyStatus === 'completed' + ? 2 + : studyStatus === 'savedforlater' + ? 1 + : 0; + + cy.get('button').each((button, index) => { + if (index === buttonIndex) { + cy.wrap(button).should( + 'have.class', + 'MuiButton-contained' + ); + } else { + cy.wrap(button).should( + 'have.class', + 'MuiButton-outlined' + ); + } + }); + }); + }); + }); + }); + }); + }); + + it('should sort by status asc', () => { + cy.contains('Status').click(); + cy.get('[data-testid="ArrowDownwardIcon"]').click(); + cy.get('[data-testid="ArrowUpwardIcon"]').should('exist'); + + cy.wait('@projectFixture').then((projectFixture) => { + const project = projectFixture?.response?.body as INeurosynthProjectReturn; + const studyStatusList = project.provenance.extractionMetadata.studyStatusList; + + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + const studies = [...(studyset.studies || [])] as StudyReturn[]; + + const sortedStudies: IExtractionTableStudy[] = studies + .map((study) => ({ + ...study, + status: studyStatusList.find((status) => status.id === study.id) + ?.status, + })) + .sort((a, b) => + (b?.status || '').localeCompare(a?.status || '', undefined, { + numeric: true, + }) + ); + + cy.get('tbody > tr').each((tr, index) => { + cy.wrap(tr).within(() => { + cy.get('td') + .eq(6) + .within(() => { + const studyStatus = sortedStudies[index].status; + const buttonIndex = + studyStatus === 'completed' + ? 2 + : studyStatus === 'savedforlater' + ? 1 + : 0; + + cy.get('button').each((button, index) => { + if (index === buttonIndex) { + cy.wrap(button).should( + 'have.class', + 'MuiButton-contained' + ); + } else { + cy.wrap(button).should( + 'have.class', + 'MuiButton-outlined' + ); + } + }); + }); + }); + }); + }); + }); + }); + }); + + describe.only('pagination', () => { + beforeEach(() => { + cy.fixture('studyset').then((studyset) => { + // as we are artificially creating new studies below, the out of sync popup wil appear. That's expected and + // we can just ignore it during out test + console.log(studyset); + const studies = []; + for (let i = 0; i < 100; i++) { + studies.push(...(studyset?.studies as StudyReturn[])); + } + studyset.studies = studies; + console.log(studyset); + cy.intercept('GET', `**/api/studysets/*`, studyset).as('studysetFixture'); + }); + }); + + beforeEach(() => { + cy.login('mocked').visit(`/projects/abc123/extraction`); + }); + + it('should give the correct number of studies', () => { + cy.wait('@studysetFixture').then((studysetFixture) => { + const studyset = studysetFixture.response?.body as StudysetReturn; + cy.contains(`Total: ${studyset.studies?.length} studies`).should('exist'); + }); + }); + + it('should change the number of rows per page', () => { + cy.get('[role="combobox"]').eq(2).click(); + cy.get('div[role="presentation"]').within(() => { + cy.contains('10').click(); + }); + cy.get('tbody > tr').should('have.length', 10); + cy.get('.MuiPaginationItem-root').contains('30'); + + cy.get('[role="combobox"]').eq(2).click(); + cy.get('div[role="presentation"]').within(() => { + cy.contains('25').click(); + }); + cy.get('tbody > tr').should('have.length', 25); + cy.get('.MuiPaginationItem-root').contains('12'); + + cy.get('[role="combobox"]').eq(2).click(); + cy.get('div[role="presentation"]').within(() => { + cy.contains('50').click(); + }); + cy.get('tbody > tr').should('have.length', 50); + + cy.get('[role="combobox"]').eq(2).click(); + cy.get('div[role="presentation"]').within(() => { + cy.contains('100').click(); + }); + cy.get('tbody > tr').should('have.length', 100); + cy.get('.MuiPaginationItem-root').contains('3'); + }); + }); + + describe('navigation', () => { + beforeEach(() => { + cy.login('mocked').visit(`/projects/abc123/extraction`); + }); + + it('should navigate to the selected study with the saved table state', () => { + cy.get('tbody > tr').eq(0).click(); + cy.url().should('include', `/projects/abc123/extraction/studies/3Jvrv4Pct3hb/edit`); + + cy.window().then((window) => { + const item = window.sessionStorage.getItem(`abc123-extraction-table`); + cy.wrap(item).should('exist'); + }); + }); + + it('should keep the table state', () => { + cy.get('tbody > tr').eq(0).click(); + cy.url().should('include', `/projects/abc123/extraction/studies/3Jvrv4Pct3hb/edit`); + + cy.visit(`/projects/abc123/extraction`); + + cy.window().then((window) => { + const item = window.sessionStorage.getItem(`abc123-extraction-table`); + cy.wrap(item).should('exist'); + }); + }); + + it('should save the filter and sorting to the table state', () => { + cy.contains('Year').click(); + cy.get('input').eq(1).click(); + cy.get(`input`).eq(1).type('Activation'); + + // we wait because of the debounce + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(800); + + cy.get('tbody > tr').eq(0).click(); + + cy.window().then((window) => { + const state = window.sessionStorage.getItem(`abc123-extraction-table`); + const parsedState = JSON.parse(state || '{}'); + console.log(parsedState); + cy.wrap(parsedState).should('deep.equal', { + columnFilters: [{ id: 'name', value: 'Activation' }], + sorting: [{ id: 'year', desc: true }], + studies: ['3zutS8kyg2sy'], + }); + }); + }); + }); +}); diff --git a/compose/neurosynth-frontend/cypress/e2e/workflows/MetaAnalyses/CreateSpecificationDialog.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/workflows/MetaAnalyses/CreateSpecificationDialog.cy.tsx index 0be5448e..7e42430c 100644 --- a/compose/neurosynth-frontend/cypress/e2e/workflows/MetaAnalyses/CreateSpecificationDialog.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/MetaAnalyses/CreateSpecificationDialog.cy.tsx @@ -2,7 +2,7 @@ describe('CreateSpecificationDialog', () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/meta-analyses*`, { fixture: 'metaAnalyses' }).as( 'metaAnalysesFixture' diff --git a/compose/neurosynth-frontend/cypress/e2e/workflows/SleuthImport/DoSleuthImport.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/workflows/SleuthImport/DoSleuthImport.cy.tsx index 516a357c..c25014cf 100644 --- a/compose/neurosynth-frontend/cypress/e2e/workflows/SleuthImport/DoSleuthImport.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/SleuthImport/DoSleuthImport.cy.tsx @@ -14,7 +14,7 @@ describe('DoSleuthImport', () => { const neurosynthAPIBaseURL = Cypress.env('neurosynthAPIBaseURL'); beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('POST', `https://www.google-analytics.com/*/**`, {}).as( 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 6a2d6ab7..fb90cc61 100644 --- a/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx +++ b/compose/neurosynth-frontend/cypress/e2e/workflows/ingestion/Ingestion.cy.tsx @@ -4,7 +4,7 @@ const PATH = '/projects/mock-project-id/curation'; describe('Ingestion', () => { beforeEach(() => { - cy.clearLocalStorage().clearSessionStorage(); + cy.clearLocalStorage(); cy.intercept('GET', 'https://api.appzi.io/**', { fixture: 'appzi' }).as('appziFixture'); cy.intercept('GET', `**/api/meta-analyses*`, { fixture: 'metaAnalyses' }).as( 'metaAnalysesFixture' diff --git a/compose/neurosynth-frontend/cypress/fixtures/Extraction/project.json b/compose/neurosynth-frontend/cypress/fixtures/Extraction/project.json new file mode 100644 index 00000000..c7d4209e --- /dev/null +++ b/compose/neurosynth-frontend/cypress/fixtures/Extraction/project.json @@ -0,0 +1,184 @@ +{ + "created_at": "2023-11-02T15:48:04.630172+00:00", + "description": "this is a bulk import test", + "id": "abc123", + "meta_analyses": ["3xHJJQWFURob"], + "name": "Bulk import test", + "neurostore_study": { + "created_at": "2023-11-02T15:48:04.640598+00:00", + "exception": null, + "neurostore_id": "7BrAuwJ2tjPW", + "status": "PENDING", + "traceback": null, + "updated_at": "2023-11-02T15:48:04.653465+00:00" + }, + "neurostore_url": "https://neurostore.org/api/studies/7BrAuwJ2tjPW", + "provenance": { + "curationMetadata": { + "columns": [ + { + "id": "370ba40c-91e8-47e7-9b4d-926bc8f31d10", + "name": "not included", + "stubStudies": [] + }, + { + "id": "1f15d9a7-968e-44a6-8574-caa3794e050e", + "name": "included", + "stubStudies": [ + { + "abstractText": "", + "articleLink": "https://pubmed.ncbi.nlm.nih.gov/9808463", + "articleYear": "1998", + "authors": "Corbetta M, Akbudak E, Conturo TE, Snyder AZ, Ollinger JM, Drury HA, Linenweber MR, Petersen SE, Raichle ME, Van Essen DC, Shulman GL", + "doi": "10.1016/S0896-6273(00)80593-0", + "exclusionTag": null, + "id": "1", + "identificationSource": { + "id": "neurosynth_neurostore_id_source", + "label": "Neurostore" + }, + "journal": "Neuron", + "keywords": "", + "neurostoreId": "3Jvrv4Pct3hb", + "pmcid": "", + "pmid": "9808463", + "searchTerm": "?genericSearchStr=psychosis&dataType=all&source=all&sortBy=relevance&descOrder=true", + "tags": [], + "title": "A common network of functional areas for attention and eye movements." + }, + { + "abstractText": "Important decisions are often made under stressful\r\ncircumstances thatmight compromise self-regulatory\r\nbehavior. Yet the neural mechanisms by which\r\nstress influences self-control choices are unclear.\r\nWe investigated these mechanisms in human participants\r\nwho faced self-control dilemmas over food\r\nreward while undergoing fMRI following stress.\r\nWe found that stress increased the influence of\r\nimmediately rewarding taste attributes on choice\r\nand reduced self-control. This choice pattern was\r\naccompanied by increased functional connectivity\r\nbetween ventromedial prefrontal cortex (vmPFC)\r\nand amygdala and striatal regions encoding tastiness.\r\nFurthermore, stress was associated with\r\nreduced connectivity between the vmPFC and\r\ndorsolateral prefrontal cortex regions linked to selfcontrol\r\nsuccess. Notably, alterations in connectivity\r\npathways could be dissociated by their differential\r\nrelationships with cortisol and perceived stress.\r\nOur results indicate that stress may compromise\r\nself-control decisions by both enhancing the impact\r\nof immediately rewarding attributes and reducing the\r\nefficacy of regions promoting behaviors that are\r\nconsistent with long-term goals.\r\n\r\nThis collection contains second level correlations of stress induced differences in the influence of taste based decisions on vStr,Amyg and vmPFC and their connectivity.\r\n\r\nKey words: food choice, decision making, self-regulation", + "articleLink": "", + "articleYear": "", + "authors": "Silvia U. Maier, Aidan B. Makwana and Todd A. Hare", + "doi": "10.1016/j.neuron.2015.07.005", + "exclusionTag": null, + "id": "2", + "identificationSource": { + "id": "neurosynth_neurostore_id_source", + "label": "Neurostore" + }, + "journal": "Neuron", + "keywords": "", + "neurostoreId": "36nHEsLLPwBB", + "pmcid": "", + "pmid": "26247866", + "searchTerm": "?genericSearchStr=psychosis&dataType=all&source=all&sortBy=relevance&descOrder=true", + "tags": [], + "title": "Acute Stress Impairs Self-Control in Goal-Directed Choice by Altering Multiple Functional Connections within the Brain’s Decision Circuits" + }, + { + "abstractText": "", + "articleLink": "", + "articleYear": "", + "authors": "Ethan M. McCormick, Yang Qu and Eva H. Telzer", + "doi": "10.3389/fnhum.2017.00141", + "exclusionTag": null, + "id": "3", + "identificationSource": { + "id": "neurosynth_neurostore_id_source", + "label": "Neurostore" + }, + "journal": "Frontiers in Human Neuroscience", + "keywords": "", + "neurostoreId": "3zutS8kyg2sy", + "pmcid": "", + "pmid": "", + "searchTerm": "?genericSearchStr=psychosis&dataType=all&source=all&sortBy=relevance&descOrder=true", + "tags": [], + "title": "Activation in Context: Differential Conclusions Drawn from Cross-Sectional and Longitudinal Analyses of Adolescents’ Cognitive Control-Related Neural Activity" + } + ] + } + ], + "exclusionTags": [ + { + "id": "neurosynth_exclude_exclusion", + "isAssignable": true, + "isExclusionTag": true, + "label": "Exclude" + }, + { + "id": "neurosynth_duplicate_exclusion", + "isAssignable": true, + "isExclusionTag": true, + "label": "Duplicate" + } + ], + "identificationSources": [ + { + "id": "neurosynth_neurostore_id_source", + "label": "Neurostore" + }, + { + "id": "neurosynth_pubmed_id_source", + "label": "PubMed" + }, + { + "id": "neurosynth_scopus_id_source", + "label": "Scopus" + }, + { + "id": "neurosynth_web_of_science_id_source", + "label": "Web of Science" + }, + { + "id": "neurosynth_psycinfo_id_source", + "label": "PsycInfo" + } + ], + "infoTags": [ + { + "id": "neurosynth_untagged_tag", + "isAssignable": false, + "isExclusionTag": false, + "label": "Untagged studies" + }, + { + "id": "neurosynth_uncategorized_tag", + "isAssignable": false, + "isExclusionTag": false, + "label": "Uncategorized Studies" + }, + { + "id": "neurosynth_needs_review_tag", + "isAssignable": false, + "isExclusionTag": false, + "label": "Needs Review" + }, + { + "id": "6f299c47-766c-48bd-a56b-9c77019ea9de", + "isAssignable": true, + "isExclusionTag": false, + "label": "marijuana" + } + ], + "prismaConfig": { + "eligibility": { + "exclusionTags": [] + }, + "identification": { + "exclusionTags": [] + }, + "isPrisma": false, + "screening": { + "exclusionTags": [] + } + } + }, + "extractionMetadata": { + "annotationId": "5LSBDTGqA6RF", + "studyStatusList": [ + { "id": "3zutS8kyg2sy", "status": "completed" } + ], + "studysetId": "73HRs8HaJbR8" + }, + "metaAnalysisMetadata": { + "canEditMetaAnalyses": false + } + }, + "public": false, + "updated_at": "2023-11-02T19:42:04.265234+00:00", + "user": "auth0|62e0e6c9dd47048572613b4d", + "username": "Test User" +} diff --git a/compose/neurosynth-frontend/cypress/fixtures/studyset.json b/compose/neurosynth-frontend/cypress/fixtures/studyset.json index 0335eaf3..5f75b2f6 100644 --- a/compose/neurosynth-frontend/cypress/fixtures/studyset.json +++ b/compose/neurosynth-frontend/cypress/fixtures/studyset.json @@ -1,660 +1,606 @@ { - "id":"5ATjENA3VVyE", - "name":"a test studyset", - "user":"auth0|62de78bc11222b208cd022c8", - "description":"some new studyset", - "publication":null, - "doi":null, - "pmid":null, - "created_at":"2022-07-25T11:08:25.999097+00:00", - "updated_at":null, - "studies":[ + "id": "5ATjENA3VVyE", + "name": "a test studyset", + "user": "auth0|62de78bc11222b208cd022c8", + "description": "some new studyset", + "publication": null, + "doi": null, + "pmid": null, + "created_at": "2022-07-25T11:08:25.999097+00:00", + "updated_at": null, + "studies": [ { - "id":"3Jvrv4Pct3hb", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "name":"A common network of functional areas for attention and eye movements.", - "description":null, - "publication":"Neuron", - "doi":"10.1016/S0896-6273(00)80593-0", - "pmid":"9808463", - "authors":"Corbetta M, Akbudak E, Conturo TE, Snyder AZ, Ollinger JM, Drury HA, Linenweber MR, Petersen SE, Raichle ME, Van Essen DC, Shulman GL", - "year":1998, - "metadata":null, - "source":"neurosynth", - "source_id":"9808463", - "source_updated_at":null, - "analyses":[ + "id": "3Jvrv4Pct3hb", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "name": "A common network of functional areas for attention and eye movements.", + "description": null, + "publication": "Neuron", + "doi": "10.1016/S0896-6273(00)80593-0", + "pmid": "9808463", + "authors": "Corbetta M, Akbudak E, Conturo TE, Snyder AZ, Ollinger JM, Drury HA, Linenweber MR, Petersen SE, Raichle ME, Van Essen DC, Shulman GL", + "year": 1998, + "metadata": null, + "source": "neurosynth", + "source_id": "9808463", + "source_updated_at": null, + "analyses": [ { - "id":"6h7WzZ7sXd7S", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "study":"3Jvrv4Pct3hb", - "name":"35975", - "description":null, - "conditions":[ - - ], - "weights":[ - - ], - "points":[ + "id": "6h7WzZ7sXd7S", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "study": "3Jvrv4Pct3hb", + "name": "35975", + "description": null, + "conditions": [], + "weights": [], + "points": [ { - "id":"7bUZdF3z59wk", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "coordinates":[ - 6.0, - -49.0, - 10.0 - ], - "analysis":"6h7WzZ7sXd7S", - "kind":"unknown", - "space":"TAL", - "image":null, - "label_id":null, - "entities":[ + "id": "7bUZdF3z59wk", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "coordinates": [6.0, -49.0, 10.0], + "analysis": "6h7WzZ7sXd7S", + "kind": "unknown", + "space": "TAL", + "image": null, + "label_id": null, + "entities": [ { - "id":"4BPFBTeFwqWj", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "level":"group", - "label":"35975", - "analysis":"6h7WzZ7sXd7S" + "id": "4BPFBTeFwqWj", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "level": "group", + "label": "35975", + "analysis": "6h7WzZ7sXd7S" } ], - "values":[ - - ] + "values": [] }, { - "id":"4GbFME36aBgD", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "coordinates":[ - 6.0, - -67.0, - 8.0 - ], - "analysis":"6h7WzZ7sXd7S", - "kind":"unknown", - "space":"TAL", - "image":null, - "label_id":null, - "entities":[ + "id": "4GbFME36aBgD", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "coordinates": [6.0, -67.0, 8.0], + "analysis": "6h7WzZ7sXd7S", + "kind": "unknown", + "space": "TAL", + "image": null, + "label_id": null, + "entities": [ { - "id":"6tmukHRo3zZB", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "level":"group", - "label":"35975", - "analysis":"6h7WzZ7sXd7S" + "id": "6tmukHRo3zZB", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "level": "group", + "label": "35975", + "analysis": "6h7WzZ7sXd7S" } ], - "values":[ - - ] + "values": [] }, { - "id":"6RZfQRn4KBa9", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "coordinates":[ - 6.0, - -67.0, - 8.0 - ], - "analysis":"6h7WzZ7sXd7S", - "kind":"unknown", - "space":"TAL", - "image":null, - "label_id":null, - "entities":[ + "id": "6RZfQRn4KBa9", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "coordinates": [6.0, -67.0, 8.0], + "analysis": "6h7WzZ7sXd7S", + "kind": "unknown", + "space": "TAL", + "image": null, + "label_id": null, + "entities": [ { - "id":"3ko2zEd4Adjf", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "level":"group", - "label":"35975", - "analysis":"6h7WzZ7sXd7S" + "id": "3ko2zEd4Adjf", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "level": "group", + "label": "35975", + "analysis": "6h7WzZ7sXd7S" } ], - "values":[ - - ] + "values": [] }, { - "id":"849pEYuZMgtH", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "user":null, - "coordinates":[ - -37.0, - -7.0, - 46.0 - ], - "analysis":"6h7WzZ7sXd7S", - "kind":"unknown", - "space":"TAL", - "image":null, - "label_id":null, - "entities":[ + "id": "849pEYuZMgtH", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "user": null, + "coordinates": [-37.0, -7.0, 46.0], + "analysis": "6h7WzZ7sXd7S", + "kind": "unknown", + "space": "TAL", + "image": null, + "label_id": null, + "entities": [ { - "id":"PmrzkXkgkLn7", - "created_at":"2022-07-22T08:26:40.465069+00:00", - "updated_at":null, - "level":"group", - "label":"35975", - "analysis":"6h7WzZ7sXd7S" + "id": "PmrzkXkgkLn7", + "created_at": "2022-07-22T08:26:40.465069+00:00", + "updated_at": null, + "level": "group", + "label": "35975", + "analysis": "6h7WzZ7sXd7S" } ], - "values":[ - - ] + "values": [] } ], - "images":[ - - ] + "images": [] } ] }, { - "id":"36nHEsLLPwBB", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "name":"Acute Stress Impairs Self-Control in Goal-Directed Choice by Altering Multiple Functional Connections within the Brain’s Decision Circuits", - "description":"Important decisions are often made under stressful\r\ncircumstances thatmight compromise self-regulatory\r\nbehavior. Yet the neural mechanisms by which\r\nstress influences self-control choices are unclear.\r\nWe investigated these mechanisms in human participants\r\nwho faced self-control dilemmas over food\r\nreward while undergoing fMRI following stress.\r\nWe found that stress increased the influence of\r\nimmediately rewarding taste attributes on choice\r\nand reduced self-control. This choice pattern was\r\naccompanied by increased functional connectivity\r\nbetween ventromedial prefrontal cortex (vmPFC)\r\nand amygdala and striatal regions encoding tastiness.\r\nFurthermore, stress was associated with\r\nreduced connectivity between the vmPFC and\r\ndorsolateral prefrontal cortex regions linked to selfcontrol\r\nsuccess. Notably, alterations in connectivity\r\npathways could be dissociated by their differential\r\nrelationships with cortisol and perceived stress.\r\nOur results indicate that stress may compromise\r\nself-control decisions by both enhancing the impact\r\nof immediately rewarding attributes and reducing the\r\nefficacy of regions promoting behaviors that are\r\nconsistent with long-term goals.\r\n\r\nThis collection contains second level correlations of stress induced differences in the influence of taste based decisions on vStr,Amyg and vmPFC and their connectivity.\r\n\r\nKey words: food choice, decision making, self-regulation", - "publication":"Neuron", - "doi":"10.1016/j.neuron.2015.07.005", - "pmid":null, - "authors":"Silvia U. Maier, Aidan B. Makwana and Todd A. Hare", - "year":null, - "metadata":{ - "url":"https://neurovault.org/collections/3259/", - "download_url":"https://neurovault.org/collections/3259/download", - "owner":1349, - "contributors":"HareLab", - "owner_name":"silvia.maier", - "number_of_images":4, - "paper_url":"https://linkinghub.elsevier.com/retrieve/pii/S0896627315006273", - "full_dataset_url":"", - "private":false, - "add_date":"2017-12-12T13:45:50.068678Z", - "modify_date":"2018-11-05T08:22:52.152005Z", - "doi_add_date":"2017-12-12T13:45:50.068157Z", - "type_of_design":"eventrelated", - "number_of_imaging_runs":3, - "number_of_experimental_units":70, - "length_of_runs":null, - "length_of_blocks":null, - "length_of_trials":"variable", - "optimization":true, - "optimization_method":"", - "subject_age_mean":21.15, - "subject_age_min":18.0, - "subject_age_max":27.0, - "handedness":"right", - "proportion_male_subjects":1.0, - "inclusion_exclusion_criteria":"normal or corrected-to-normal vision, non-smokers and refrained from taking any medication for 3 days prior to their scanning session,no history of eating disorders or food allergies and intolerances", - "number_of_rejected_subjects":3, - "group_comparison":true, - "group_description":"stress induction via socially evaluated cold pressor test vs control group (warm water condition)", - "scanner_make":"Phillips", - "scanner_model":"Philips Achieva 3 T whole-body scanner", - "field_strength":3.0, - "pulse_sequence":"T2* EPI ", - "parallel_imaging":"SENSE factor 2", - "field_of_view":null, - "matrix_size":null, - "slice_thickness":3.1, - "skip_distance":0.6, - "acquisition_orientation":"axial at +15 degree tilt to ACPC", - "order_of_acquisition":"ascending", - "repetition_time":2460.0, - "echo_time":30.0, - "flip_angle":77.0, - "software_package":"", - "software_version":"", - "order_of_preprocessing_operations":"", - "quality_control":"", - "used_b0_unwarping":null, - "b0_unwarping_software":"", - "used_slice_timing_correction":null, - "slice_timing_correction_software":"", - "used_motion_correction":null, - "motion_correction_software":"", - "motion_correction_reference":"", - "motion_correction_metric":"", - "motion_correction_interpolation":"", - "used_motion_susceptibiity_correction":null, - "used_intersubject_registration":null, - "intersubject_registration_software":"", - "intersubject_transformation_type":null, - "nonlinear_transform_type":"", - "transform_similarity_metric":"", - "interpolation_method":"", - "object_image_type":"", - "functional_coregistered_to_structural":null, - "functional_coregistration_method":"", - "coordinate_space":null, - "target_template_image":"", - "target_resolution":null, - "used_smoothing":null, - "smoothing_type":"", - "smoothing_fwhm":null, - "resampled_voxel_size":null, - "intrasubject_model_type":"", - "intrasubject_estimation_type":"", - "intrasubject_modeling_software":"", - "hemodynamic_response_function":"", - "used_temporal_derivatives":null, - "used_dispersion_derivatives":null, - "used_motion_regressors":null, - "used_reaction_time_regressor":null, - "used_orthogonalization":null, - "orthogonalization_description":"", - "used_high_pass_filter":null, - "high_pass_filter_method":"", - "autocorrelation_model":"", - "group_model_type":"", - "group_estimation_type":"", - "group_modeling_software":"", - "group_inference_type":null, - "group_model_multilevel":"", - "group_repeated_measures":null, - "group_repeated_measures_method":"", - "nutbrain_hunger_state":"II", - "nutbrain_food_viewing_conditions":"palatable-healthy, unpalatable-healthy, palatable-unhealthy, unpalatable-unhealthy", - "nutbrain_food_choice_type":"two-alternative forced choice, all combinations of the above foods / image categories possible", - "nutbrain_taste_conditions":"none", - "nutbrain_odor_conditions":"none", - "communities":[ - 2 - ] + "id": "36nHEsLLPwBB", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "name": "Acute Stress Impairs Self-Control in Goal-Directed Choice by Altering Multiple Functional Connections within the Brain’s Decision Circuits", + "description": "Important decisions are often made under stressful\r\ncircumstances thatmight compromise self-regulatory\r\nbehavior. Yet the neural mechanisms by which\r\nstress influences self-control choices are unclear.\r\nWe investigated these mechanisms in human participants\r\nwho faced self-control dilemmas over food\r\nreward while undergoing fMRI following stress.\r\nWe found that stress increased the influence of\r\nimmediately rewarding taste attributes on choice\r\nand reduced self-control. This choice pattern was\r\naccompanied by increased functional connectivity\r\nbetween ventromedial prefrontal cortex (vmPFC)\r\nand amygdala and striatal regions encoding tastiness.\r\nFurthermore, stress was associated with\r\nreduced connectivity between the vmPFC and\r\ndorsolateral prefrontal cortex regions linked to selfcontrol\r\nsuccess. Notably, alterations in connectivity\r\npathways could be dissociated by their differential\r\nrelationships with cortisol and perceived stress.\r\nOur results indicate that stress may compromise\r\nself-control decisions by both enhancing the impact\r\nof immediately rewarding attributes and reducing the\r\nefficacy of regions promoting behaviors that are\r\nconsistent with long-term goals.\r\n\r\nThis collection contains second level correlations of stress induced differences in the influence of taste based decisions on vStr,Amyg and vmPFC and their connectivity.\r\n\r\nKey words: food choice, decision making, self-regulation", + "publication": "Neuron", + "doi": "10.1016/j.neuron.2015.07.005", + "pmid": null, + "authors": "Silvia U. Maier, Aidan B. Makwana and Todd A. Hare", + "year": 1999, + "metadata": { + "url": "https://neurovault.org/collections/3259/", + "download_url": "https://neurovault.org/collections/3259/download", + "owner": 1349, + "contributors": "HareLab", + "owner_name": "silvia.maier", + "number_of_images": 4, + "paper_url": "https://linkinghub.elsevier.com/retrieve/pii/S0896627315006273", + "full_dataset_url": "", + "private": false, + "add_date": "2017-12-12T13:45:50.068678Z", + "modify_date": "2018-11-05T08:22:52.152005Z", + "doi_add_date": "2017-12-12T13:45:50.068157Z", + "type_of_design": "eventrelated", + "number_of_imaging_runs": 3, + "number_of_experimental_units": 70, + "length_of_runs": null, + "length_of_blocks": null, + "length_of_trials": "variable", + "optimization": true, + "optimization_method": "", + "subject_age_mean": 21.15, + "subject_age_min": 18.0, + "subject_age_max": 27.0, + "handedness": "right", + "proportion_male_subjects": 1.0, + "inclusion_exclusion_criteria": "normal or corrected-to-normal vision, non-smokers and refrained from taking any medication for 3 days prior to their scanning session,no history of eating disorders or food allergies and intolerances", + "number_of_rejected_subjects": 3, + "group_comparison": true, + "group_description": "stress induction via socially evaluated cold pressor test vs control group (warm water condition)", + "scanner_make": "Phillips", + "scanner_model": "Philips Achieva 3 T whole-body scanner", + "field_strength": 3.0, + "pulse_sequence": "T2* EPI ", + "parallel_imaging": "SENSE factor 2", + "field_of_view": null, + "matrix_size": null, + "slice_thickness": 3.1, + "skip_distance": 0.6, + "acquisition_orientation": "axial at +15 degree tilt to ACPC", + "order_of_acquisition": "ascending", + "repetition_time": 2460.0, + "echo_time": 30.0, + "flip_angle": 77.0, + "software_package": "", + "software_version": "", + "order_of_preprocessing_operations": "", + "quality_control": "", + "used_b0_unwarping": null, + "b0_unwarping_software": "", + "used_slice_timing_correction": null, + "slice_timing_correction_software": "", + "used_motion_correction": null, + "motion_correction_software": "", + "motion_correction_reference": "", + "motion_correction_metric": "", + "motion_correction_interpolation": "", + "used_motion_susceptibiity_correction": null, + "used_intersubject_registration": null, + "intersubject_registration_software": "", + "intersubject_transformation_type": null, + "nonlinear_transform_type": "", + "transform_similarity_metric": "", + "interpolation_method": "", + "object_image_type": "", + "functional_coregistered_to_structural": null, + "functional_coregistration_method": "", + "coordinate_space": null, + "target_template_image": "", + "target_resolution": null, + "used_smoothing": null, + "smoothing_type": "", + "smoothing_fwhm": null, + "resampled_voxel_size": null, + "intrasubject_model_type": "", + "intrasubject_estimation_type": "", + "intrasubject_modeling_software": "", + "hemodynamic_response_function": "", + "used_temporal_derivatives": null, + "used_dispersion_derivatives": null, + "used_motion_regressors": null, + "used_reaction_time_regressor": null, + "used_orthogonalization": null, + "orthogonalization_description": "", + "used_high_pass_filter": null, + "high_pass_filter_method": "", + "autocorrelation_model": "", + "group_model_type": "", + "group_estimation_type": "", + "group_modeling_software": "", + "group_inference_type": null, + "group_model_multilevel": "", + "group_repeated_measures": null, + "group_repeated_measures_method": "", + "nutbrain_hunger_state": "II", + "nutbrain_food_viewing_conditions": "palatable-healthy, unpalatable-healthy, palatable-unhealthy, unpalatable-unhealthy", + "nutbrain_food_choice_type": "two-alternative forced choice, all combinations of the above foods / image categories possible", + "nutbrain_taste_conditions": "none", + "nutbrain_odor_conditions": "none", + "communities": [2] }, - "source":"neurovault", - "source_id":"3259", - "source_updated_at":null, - "analyses":[ + "source": "neurovault", + "source_id": "3259", + "source_updated_at": null, + "analyses": [ { - "id":"5pDk47P9JdU5", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "study":"36nHEsLLPwBB", - "name":"Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", - "description":"The statistical parametric maps show two regions of the vStr (left) and Amyg (right) where the correlation with relative taste value is higher in the stress compared to control group (p < 0.05 SVC). The color scale represents t statistics derived from 5,000 permutations of the data.", - "conditions":[ + "id": "5pDk47P9JdU5", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "study": "36nHEsLLPwBB", + "name": "Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", + "description": "The statistical parametric maps show two regions of the vStr (left) and Amyg (right) where the correlation with relative taste value is higher in the stress compared to control group (p < 0.05 SVC). The color scale represents t statistics derived from 5,000 permutations of the data.", + "conditions": [ { - "id":"8EvU75j2xga2", - "user":null, - "name":"None / Other", - "description":null, - "created_at":"2022-07-22T08:27:23.612916+00:00", - "updated_at":null + "id": "8EvU75j2xga2", + "user": null, + "name": "None / Other", + "description": null, + "created_at": "2022-07-22T08:27:23.612916+00:00", + "updated_at": null } ], - "weights":[ - 1.0 - ], - "points":[ - - ], - "images":[ + "weights": [1.0], + "points": [], + "images": [ { - "id":"6M6LMLxB4BLx", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "analysis":"5pDk47P9JdU5", - "analysis_name":"Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", - "entities":[ + "id": "6M6LMLxB4BLx", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "analysis": "5pDk47P9JdU5", + "analysis_name": "Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", + "entities": [ { - "id":"6M6LMLxB4BLx", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "level":"group", - "label":"Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", - "analysis":"5pDk47P9JdU5" + "id": "6M6LMLxB4BLx", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "level": "group", + "label": "Figure 3. Stress-Induced Differences in the Influence of Taste on Self-Control Choice Behavior and Neural Activity", + "analysis": "5pDk47P9JdU5" } ], - "url":"https://neurovault.org/media/images/3259/Tc-Tnc_amy_str_masked_tstat1.nii.gz", - "space":"MNI", - "value_type":"T", - "filename":"Tc-Tnc_amy_str_masked_tstat1.nii.gz", - "add_date":"2017-12-12T13:53:26.590375+00:00" + "url": "https://neurovault.org/media/images/3259/Tc-Tnc_amy_str_masked_tstat1.nii.gz", + "space": "MNI", + "value_type": "T", + "filename": "Tc-Tnc_amy_str_masked_tstat1.nii.gz", + "add_date": "2017-12-12T13:53:26.590375+00:00" } ] }, { - "id":"7tCkPNYJnUfW", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "study":"36nHEsLLPwBB", - "name":"Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", - "description":"The statistical parametric map shows areas of the vStr (upper) and Amyg (lower) where the increase in functional connectivity with vmPFC on trials in which the tastier item was chosen is greater for stress than control participants (p < 0.05 SVC). The color scale represents t statistics derived from 5,000 permutations of the data.", - "conditions":[ + "id": "7tCkPNYJnUfW", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "study": "36nHEsLLPwBB", + "name": "Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", + "description": "The statistical parametric map shows areas of the vStr (upper) and Amyg (lower) where the increase in functional connectivity with vmPFC on trials in which the tastier item was chosen is greater for stress than control participants (p < 0.05 SVC). The color scale represents t statistics derived from 5,000 permutations of the data.", + "conditions": [ { - "id":"8EvU75j2xga2", - "user":null, - "name":"None / Other", - "description":null, - "created_at":"2022-07-22T08:27:23.612916+00:00", - "updated_at":null + "id": "8EvU75j2xga2", + "user": null, + "name": "None / Other", + "description": null, + "created_at": "2022-07-22T08:27:23.612916+00:00", + "updated_at": null } ], - "weights":[ - 1.0 - ], - "points":[ - - ], - "images":[ + "weights": [1.0], + "points": [], + "images": [ { - "id":"5YFCTTYRhdor", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "analysis":"7tCkPNYJnUfW", - "analysis_name":"Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", - "entities":[ + "id": "5YFCTTYRhdor", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "analysis": "7tCkPNYJnUfW", + "analysis_name": "Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", + "entities": [ { - "id":"5YFCTTYRhdor", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "level":"group", - "label":"Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", - "analysis":"7tCkPNYJnUfW" + "id": "5YFCTTYRhdor", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "level": "group", + "label": "Figure 4. Stress Induction Resulted in Greater Functional Connectivity between the vmPFC and vStr and Amyg when Choosing the Tastier Food", + "analysis": "7tCkPNYJnUfW" } ], - "url":"https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1.nii.gz", - "space":"MNI", - "value_type":"T", - "filename":"vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1.nii.gz", - "add_date":"2017-12-12T14:01:51.656765+00:00" + "url": "https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1.nii.gz", + "space": "MNI", + "value_type": "T", + "filename": "vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1.nii.gz", + "add_date": "2017-12-12T14:01:51.656765+00:00" } ] }, { - "id":"34tFPBsAE2QF", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "study":"36nHEsLLPwBB", - "name":"Figure 5. vmPFC seed region for connectivity analyses", - "description":"Contrast shows BOLD activity for the integrated subjective value of the chosen food", - "conditions":[ + "id": "34tFPBsAE2QF", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "study": "36nHEsLLPwBB", + "name": "Figure 5. vmPFC seed region for connectivity analyses", + "description": "Contrast shows BOLD activity for the integrated subjective value of the chosen food", + "conditions": [ { - "id":"8EvU75j2xga2", - "user":null, - "name":"None / Other", - "description":null, - "created_at":"2022-07-22T08:27:23.612916+00:00", - "updated_at":null + "id": "8EvU75j2xga2", + "user": null, + "name": "None / Other", + "description": null, + "created_at": "2022-07-22T08:27:23.612916+00:00", + "updated_at": null } ], - "weights":[ - 1.0 - ], - "points":[ - - ], - "images":[ + "weights": [1.0], + "points": [], + "images": [ { - "id":"3gh9LPntXL6M", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "analysis":"34tFPBsAE2QF", - "analysis_name":"Figure 5. vmPFC seed region for connectivity analyses", - "entities":[ + "id": "3gh9LPntXL6M", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "analysis": "34tFPBsAE2QF", + "analysis_name": "Figure 5. vmPFC seed region for connectivity analyses", + "entities": [ { - "id":"3gh9LPntXL6M", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "level":"group", - "label":"Figure 5. vmPFC seed region for connectivity analyses", - "analysis":"34tFPBsAE2QF" + "id": "3gh9LPntXL6M", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "level": "group", + "label": "Figure 5. vmPFC seed region for connectivity analyses", + "analysis": "34tFPBsAE2QF" } ], - "url":"https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc_all_tstat1.nii.gz", - "space":"MNI", - "value_type":"T", - "filename":"vmPFC_FV3_nohc_FVc_all_tstat1.nii.gz", - "add_date":"2017-12-12T15:10:48.317190+00:00" + "url": "https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc_all_tstat1.nii.gz", + "space": "MNI", + "value_type": "T", + "filename": "vmPFC_FV3_nohc_FVc_all_tstat1.nii.gz", + "add_date": "2017-12-12T15:10:48.317190+00:00" } ] }, { - "id":"6AdMa3eLernw", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "study":"36nHEsLLPwBB", - "name":"supplemental to Figure 5. vmPFC seed region for connectivity analyses", - "description":"BOLD contrast shows the integrated subjective relative food value (value of the chosen food - value of the non-chosen food).", - "conditions":[ + "id": "6AdMa3eLernw", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "study": "36nHEsLLPwBB", + "name": "supplemental to Figure 5. vmPFC seed region for connectivity analyses", + "description": "BOLD contrast shows the integrated subjective relative food value (value of the chosen food - value of the non-chosen food).", + "conditions": [ { - "id":"8EvU75j2xga2", - "user":null, - "name":"None / Other", - "description":null, - "created_at":"2022-07-22T08:27:23.612916+00:00", - "updated_at":null + "id": "8EvU75j2xga2", + "user": null, + "name": "None / Other", + "description": null, + "created_at": "2022-07-22T08:27:23.612916+00:00", + "updated_at": null } ], - "weights":[ - 1.0 - ], - "points":[ - - ], - "images":[ + "weights": [1.0], + "points": [], + "images": [ { - "id":"cnuvMAVybD7v", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "user":null, - "analysis":"6AdMa3eLernw", - "analysis_name":"supplemental to Figure 5. vmPFC seed region for connectivity analyses", - "entities":[ + "id": "cnuvMAVybD7v", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "user": null, + "analysis": "6AdMa3eLernw", + "analysis_name": "supplemental to Figure 5. vmPFC seed region for connectivity analyses", + "entities": [ { - "id":"cnuvMAVybD7v", - "created_at":"2022-07-22T08:29:51.625947+00:00", - "updated_at":null, - "level":"group", - "label":"supplemental to Figure 5. vmPFC seed region for connectivity analyses", - "analysis":"6AdMa3eLernw" + "id": "cnuvMAVybD7v", + "created_at": "2022-07-22T08:29:51.625947+00:00", + "updated_at": null, + "level": "group", + "label": "supplemental to Figure 5. vmPFC seed region for connectivity analyses", + "analysis": "6AdMa3eLernw" } ], - "url":"https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1_1.nii.gz", - "space":"MNI", - "value_type":"T", - "filename":"vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1_1.nii.gz", - "add_date":"2017-12-12T15:12:07.649027+00:00" + "url": "https://neurovault.org/media/images/3259/vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1_1.nii.gz", + "space": "MNI", + "value_type": "T", + "filename": "vmPFC_FV3_nohc_FVc-FVnc_pos_all_tstat1_1.nii.gz", + "add_date": "2017-12-12T15:12:07.649027+00:00" } ] } ] }, { - "id":"3zutS8kyg2sy", - "created_at":"2022-07-22T08:31:05.214847+00:00", - "updated_at":null, - "user":null, - "name":"Activation in Context: Differential Conclusions Drawn from Cross-Sectional and Longitudinal Analyses of Adolescents’ Cognitive Control-Related Neural Activity", - "description":"", - "publication":"Frontiers in Human Neuroscience", - "doi":"10.3389/fnhum.2017.00141", - "pmid":null, - "authors":"Ethan M. McCormick, Yang Qu and Eva H. Telzer", - "year":null, - "metadata":{ - "url":"https://neurovault.org/collections/2411/", - "download_url":"https://neurovault.org/collections/2411/download", - "owner":1274, - "contributors":"ehtelzer", - "owner_name":"emccormick20", - "number_of_images":1, - "paper_url":"http://journal.frontiersin.org/article/10.3389/fnhum.2017.00141/full", - "full_dataset_url":"", - "private":false, - "add_date":"2017-04-04T17:47:16.863092Z", - "modify_date":"2019-11-10T20:39:12.916636Z", - "doi_add_date":"2019-11-10T20:39:12.911623Z", - "type_of_design":null, - "number_of_imaging_runs":null, - "number_of_experimental_units":null, - "length_of_runs":null, - "length_of_blocks":null, - "length_of_trials":"", - "optimization":null, - "optimization_method":"", - "subject_age_mean":null, - "subject_age_min":null, - "subject_age_max":null, - "handedness":null, - "proportion_male_subjects":null, - "inclusion_exclusion_criteria":"", - "number_of_rejected_subjects":null, - "group_comparison":null, - "group_description":"", - "scanner_make":"", - "scanner_model":"", - "field_strength":null, - "pulse_sequence":"", - "parallel_imaging":"", - "field_of_view":null, - "matrix_size":null, - "slice_thickness":null, - "skip_distance":null, - "acquisition_orientation":"", - "order_of_acquisition":null, - "repetition_time":null, - "echo_time":null, - "flip_angle":null, - "software_package":"", - "software_version":"", - "order_of_preprocessing_operations":"", - "quality_control":"", - "used_b0_unwarping":null, - "b0_unwarping_software":"", - "used_slice_timing_correction":null, - "slice_timing_correction_software":"", - "used_motion_correction":null, - "motion_correction_software":"", - "motion_correction_reference":"", - "motion_correction_metric":"", - "motion_correction_interpolation":"", - "used_motion_susceptibiity_correction":null, - "used_intersubject_registration":null, - "intersubject_registration_software":"", - "intersubject_transformation_type":null, - "nonlinear_transform_type":"", - "transform_similarity_metric":"", - "interpolation_method":"", - "object_image_type":"", - "functional_coregistered_to_structural":null, - "functional_coregistration_method":"", - "coordinate_space":null, - "target_template_image":"", - "target_resolution":null, - "used_smoothing":null, - "smoothing_type":"", - "smoothing_fwhm":null, - "resampled_voxel_size":null, - "intrasubject_model_type":"", - "intrasubject_estimation_type":"", - "intrasubject_modeling_software":"", - "hemodynamic_response_function":"", - "used_temporal_derivatives":null, - "used_dispersion_derivatives":null, - "used_motion_regressors":null, - "used_reaction_time_regressor":null, - "used_orthogonalization":null, - "orthogonalization_description":"", - "used_high_pass_filter":null, - "high_pass_filter_method":"", - "autocorrelation_model":"", - "group_model_type":"", - "group_estimation_type":"", - "group_modeling_software":"", - "group_inference_type":null, - "group_model_multilevel":"", - "group_repeated_measures":null, - "group_repeated_measures_method":"", - "nutbrain_hunger_state":null, - "nutbrain_food_viewing_conditions":"", - "nutbrain_food_choice_type":"", - "nutbrain_taste_conditions":"", - "nutbrain_odor_conditions":"", - "communities":[ - - ] + "id": "3zutS8kyg2sy", + "created_at": "2022-07-22T08:31:05.214847+00:00", + "updated_at": null, + "user": null, + "name": "Activation in Context: Differential Conclusions Drawn from Cross-Sectional and Longitudinal Analyses of Adolescents’ Cognitive Control-Related Neural Activity", + "description": "", + "publication": "Frontiers in Human Neuroscience", + "doi": "10.3389/fnhum.2017.00141", + "pmid": "26247866", + "authors": "Ethan M. McCormick, Yang Qu and Eva H. Telzer", + "year": 2000, + "metadata": { + "url": "https://neurovault.org/collections/2411/", + "download_url": "https://neurovault.org/collections/2411/download", + "owner": 1274, + "contributors": "ehtelzer", + "owner_name": "emccormick20", + "number_of_images": 1, + "paper_url": "http://journal.frontiersin.org/article/10.3389/fnhum.2017.00141/full", + "full_dataset_url": "", + "private": false, + "add_date": "2017-04-04T17:47:16.863092Z", + "modify_date": "2019-11-10T20:39:12.916636Z", + "doi_add_date": "2019-11-10T20:39:12.911623Z", + "type_of_design": null, + "number_of_imaging_runs": null, + "number_of_experimental_units": null, + "length_of_runs": null, + "length_of_blocks": null, + "length_of_trials": "", + "optimization": null, + "optimization_method": "", + "subject_age_mean": null, + "subject_age_min": null, + "subject_age_max": null, + "handedness": null, + "proportion_male_subjects": null, + "inclusion_exclusion_criteria": "", + "number_of_rejected_subjects": null, + "group_comparison": null, + "group_description": "", + "scanner_make": "", + "scanner_model": "", + "field_strength": null, + "pulse_sequence": "", + "parallel_imaging": "", + "field_of_view": null, + "matrix_size": null, + "slice_thickness": null, + "skip_distance": null, + "acquisition_orientation": "", + "order_of_acquisition": null, + "repetition_time": null, + "echo_time": null, + "flip_angle": null, + "software_package": "", + "software_version": "", + "order_of_preprocessing_operations": "", + "quality_control": "", + "used_b0_unwarping": null, + "b0_unwarping_software": "", + "used_slice_timing_correction": null, + "slice_timing_correction_software": "", + "used_motion_correction": null, + "motion_correction_software": "", + "motion_correction_reference": "", + "motion_correction_metric": "", + "motion_correction_interpolation": "", + "used_motion_susceptibiity_correction": null, + "used_intersubject_registration": null, + "intersubject_registration_software": "", + "intersubject_transformation_type": null, + "nonlinear_transform_type": "", + "transform_similarity_metric": "", + "interpolation_method": "", + "object_image_type": "", + "functional_coregistered_to_structural": null, + "functional_coregistration_method": "", + "coordinate_space": null, + "target_template_image": "", + "target_resolution": null, + "used_smoothing": null, + "smoothing_type": "", + "smoothing_fwhm": null, + "resampled_voxel_size": null, + "intrasubject_model_type": "", + "intrasubject_estimation_type": "", + "intrasubject_modeling_software": "", + "hemodynamic_response_function": "", + "used_temporal_derivatives": null, + "used_dispersion_derivatives": null, + "used_motion_regressors": null, + "used_reaction_time_regressor": null, + "used_orthogonalization": null, + "orthogonalization_description": "", + "used_high_pass_filter": null, + "high_pass_filter_method": "", + "autocorrelation_model": "", + "group_model_type": "", + "group_estimation_type": "", + "group_modeling_software": "", + "group_inference_type": null, + "group_model_multilevel": "", + "group_repeated_measures": null, + "group_repeated_measures_method": "", + "nutbrain_hunger_state": null, + "nutbrain_food_viewing_conditions": "", + "nutbrain_food_choice_type": "", + "nutbrain_taste_conditions": "", + "nutbrain_odor_conditions": "", + "communities": [] }, - "source":"neurovault", - "source_id":"2411", - "source_updated_at":null, - "analyses":[ + "source": "neurovault", + "source_id": "2411", + "source_updated_at": null, + "analyses": [ { - "id":"5Z95x7T3TAQW", - "created_at":"2022-07-22T08:31:05.214847+00:00", - "updated_at":null, - "user":null, - "study":"3zutS8kyg2sy", - "name":"Wave 1 Nogo Trials", - "description":"", - "conditions":[ + "id": "5Z95x7T3TAQW", + "created_at": "2022-07-22T08:31:05.214847+00:00", + "updated_at": null, + "user": null, + "study": "3zutS8kyg2sy", + "name": "Wave 1 Nogo Trials", + "description": "", + "conditions": [ { - "id":"bSTJudKMNuNb", - "user":null, - "name":"go/no-go task", - "description":null, - "created_at":"2022-07-22T08:29:45.755542+00:00", - "updated_at":null + "id": "bSTJudKMNuNb", + "user": null, + "name": "go/no-go task", + "description": null, + "created_at": "2022-07-22T08:29:45.755542+00:00", + "updated_at": null } ], - "weights":[ - 1.0 - ], - "points":[ - - ], - "images":[ + "weights": [1.0], + "points": [], + "images": [ { - "id":"6jD4rqyV9sC6", - "created_at":"2022-07-22T08:31:05.214847+00:00", - "updated_at":null, - "user":null, - "analysis":"5Z95x7T3TAQW", - "analysis_name":"Wave 1 Nogo Trials", - "entities":[ + "id": "6jD4rqyV9sC6", + "created_at": "2022-07-22T08:31:05.214847+00:00", + "updated_at": null, + "user": null, + "analysis": "5Z95x7T3TAQW", + "analysis_name": "Wave 1 Nogo Trials", + "entities": [ { - "id":"6jD4rqyV9sC6", - "created_at":"2022-07-22T08:31:05.214847+00:00", - "updated_at":null, - "level":"group", - "label":"Wave 1 Nogo Trials", - "analysis":"5Z95x7T3TAQW" + "id": "6jD4rqyV9sC6", + "created_at": "2022-07-22T08:31:05.214847+00:00", + "updated_at": null, + "level": "group", + "label": "Wave 1 Nogo Trials", + "analysis": "5Z95x7T3TAQW" } ], - "url":"https://neurovault.org/media/images/2411/0001_T_Nogo_PM_T1_pos.nii.gz", - "space":"MNI", - "value_type":"T", - "filename":"0001_T_Nogo_PM_T1_pos.nii.gz", - "add_date":"2017-04-04T17:50:17.966806+00:00" + "url": "https://neurovault.org/media/images/2411/0001_T_Nogo_PM_T1_pos.nii.gz", + "space": "MNI", + "value_type": "T", + "filename": "0001_T_Nogo_PM_T1_pos.nii.gz", + "add_date": "2017-04-04T17:50:17.966806+00:00" } ] } ] } ] -} \ No newline at end of file +} diff --git a/compose/neurosynth-frontend/package-lock.json b/compose/neurosynth-frontend/package-lock.json index c5b527a4..601442bd 100644 --- a/compose/neurosynth-frontend/package-lock.json +++ b/compose/neurosynth-frontend/package-lock.json @@ -26,6 +26,7 @@ "@mui/x-data-grid": "^5.10.0", "@reactour/tour": "^2.10.3", "@sentry/react": "^7.48.0", + "@tanstack/react-table": "^8.20.5", "@types/jest": "^26.0.24", "@types/react": "^17.0.13", "@types/react-dom": "^17.0.8", @@ -4963,6 +4964,37 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", @@ -28676,6 +28708,19 @@ "tslib": "^2.4.0" } }, + "@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "requires": { + "@tanstack/table-core": "8.20.5" + } + }, + "@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==" + }, "@testing-library/dom": { "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", diff --git a/compose/neurosynth-frontend/package.json b/compose/neurosynth-frontend/package.json index 178592a2..6a105f7e 100644 --- a/compose/neurosynth-frontend/package.json +++ b/compose/neurosynth-frontend/package.json @@ -21,6 +21,7 @@ "@mui/x-data-grid": "^5.10.0", "@reactour/tour": "^2.10.3", "@sentry/react": "^7.48.0", + "@tanstack/react-table": "^8.20.5", "@types/jest": "^26.0.24", "@types/react": "^17.0.13", "@types/react-dom": "^17.0.8", diff --git a/compose/neurosynth-frontend/src/components/DebouncedTextField.tsx b/compose/neurosynth-frontend/src/components/DebouncedTextField.tsx new file mode 100644 index 00000000..45313e56 --- /dev/null +++ b/compose/neurosynth-frontend/src/components/DebouncedTextField.tsx @@ -0,0 +1,37 @@ +import { TextField, TextFieldProps } from '@mui/material'; +import { ChangeEvent, useEffect, useState } from 'react'; + +type EDebouncedTextFieldProps = Omit & { + onChange?: (value: string | undefined) => void; + value?: string | undefined; +}; + +const DebouncedTextField: React.FC = ({ + value, + onChange, + ...otherProps +}) => { + const [debouncedValue, setDebouncedValue] = useState(value || ''); + + useEffect(() => { + const debounce = setTimeout(() => { + onChange && onChange(debouncedValue); + }, 400); + return () => { + clearTimeout(debounce); + }; + }, [debouncedValue, onChange]); + + // when an update occurs from outside the component, we want to reflect that new value (like if a filter is cleard) + useEffect(() => { + setDebouncedValue(value || ''); + }, [value]); + + const handleOnChange = (event: ChangeEvent) => { + setDebouncedValue(event.target.value); + }; + + return ; +}; + +export default DebouncedTextField; diff --git a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx index aa1b9811..3fa9b23a 100644 --- a/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx +++ b/compose/neurosynth-frontend/src/components/Dialogs/ConfirmationDialog.tsx @@ -59,7 +59,8 @@ const ConfirmationDialog: React.FC = (props) => { sx={{ width: '250px' }} onClick={() => props.onCloseDialog(true, props.data)} variant="contained" - color="success" + color="primary" + disableElevation > {props.confirmText ? props.confirmText : 'Confirm'} diff --git a/compose/neurosynth-frontend/src/components/Search/SearchContainer.tsx b/compose/neurosynth-frontend/src/components/Search/SearchContainer.tsx index f9108fe8..8f7f1dc6 100644 --- a/compose/neurosynth-frontend/src/components/Search/SearchContainer.tsx +++ b/compose/neurosynth-frontend/src/components/Search/SearchContainer.tsx @@ -18,7 +18,7 @@ export interface ISearchContainer { searchMode?: 'study-search' | 'project-search'; } -const getNumTotalPages = (totalCount: number | undefined, pageSize: number | undefined) => { +export const getNumTotalPages = (totalCount: number | undefined, pageSize: number | undefined) => { if (!totalCount || !pageSize) { return 0; } diff --git a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx index 10901dfd..395d3593 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx +++ b/compose/neurosynth-frontend/src/pages/Extraction/ExtractionPage.tsx @@ -1,25 +1,19 @@ -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 { Box, Button, Typography } from '@mui/material'; import NeurosynthBreadcrumbs from 'components/NeurosynthBreadcrumbs'; import ProjectIsLoadingText from 'components/ProjectIsLoadingText'; import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; import TextEdit from 'components/TextEdit/TextEdit'; import { useGetStudysetById, useUpdateStudyset } from 'hooks'; 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, useProjectCurationColumns, - useProjectExtractionStudyStatusList, useProjectExtractionStudysetId, useProjectMetaAnalysisCanEdit, useProjectName, @@ -27,7 +21,7 @@ import { } 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 ExtractionTable from './components/ExtractionTable'; export enum EExtractionStatus { 'COMPLETED' = 'completed', @@ -35,37 +29,14 @@ export enum EExtractionStatus { 'UNCATEGORIZED' = 'uncategorized', } -const ReadOnlyStudySummaryFixedSizeListRow: React.FC< - ListChildComponentProps<{ - studies: StudyReturn[]; - currentSelectedChip: EExtractionStatus; - canEdit: boolean; - }> -> = (props) => { - const study = props.data.studies[props.index]; - const currentSelectedChip = props.data.currentSelectedChip; - const canEdit = props.data.canEdit; - - return ( - - ); -}; - const ExtractionPage: React.FC = (props) => { const { projectId } = useParams<{ projectId: string | undefined }>(); const navigate = useNavigate(); - const windowHeight = useGetWindowHeight(); useInitProjectStoreIfRequired(); const projectName = useProjectName(); const studysetId = useProjectExtractionStudysetId(); - const studyStatusList = useProjectExtractionStudyStatusList(); const columns = useProjectCurationColumns(); const loading = useGetProjectIsLoading(); const extractionSummary = useGetExtractionSummary(projectId || ''); @@ -82,20 +53,6 @@ const ExtractionPage: React.FC = (props) => { const { mutate } = useUpdateStudyset(); const [fieldBeingUpdated, setFieldBeingUpdated] = useState(''); - const selectedChipLocalStorageKey = `SELECTED_CHIP-${projectId}`; - const selectedChipInLocalStorage = - (localStorage.getItem(selectedChipLocalStorageKey) as EExtractionStatus) || - EExtractionStatus.UNCATEGORIZED; - const [currentChip, setCurrentChip] = useState(selectedChipInLocalStorage); - const [studiesDisplayedState, setStudiesDisplayedState] = useState<{ - uncategorized: StudyReturn[]; - saveForLater: StudyReturn[]; - completed: StudyReturn[]; - }>({ - uncategorized: [], - saveForLater: [], - completed: [], - }); const [showReconcilePrompt, setShowReconcilePrompt] = useState(false); useEffect(() => { @@ -109,52 +66,6 @@ const ExtractionPage: React.FC = (props) => { } }, [columns, getStudysetIsLoading, studyset?.studies, loading]); - useEffect(() => { - if (studyStatusList && studyset?.studies) { - const map = new Map(); - - studyStatusList.forEach((studyStatus) => { - map.set(studyStatus.id, studyStatus.status); - }); - - setStudiesDisplayedState((prev) => { - if (!prev) return prev; - - const allStudies = studyset.studies as StudyReturn[]; - - const completed: StudyReturn[] = []; - const saveForLater: StudyReturn[] = []; - const uncategorized: StudyReturn[] = []; - - allStudies.forEach((study) => { - if (!study?.id) return; - - if (map.has(study.id)) { - const status = map.get(study.id); - status === EExtractionStatus.COMPLETED - ? completed.push(study) - : saveForLater.push(study); - } else { - uncategorized.push(study); - } - }); - - return { - completed, - saveForLater, - uncategorized, - }; - }); - } - }, [studyStatusList, studyset?.studies]); - - const handleSelectChip = (arg: EExtractionStatus) => { - if (projectId) { - setCurrentChip(arg); - localStorage.setItem(selectedChipLocalStorageKey, arg); - } - }; - const handleUpdateStudyset = (updatedText: string, fieldName: string) => { if (studysetId) { setFieldBeingUpdated(fieldName); @@ -188,22 +99,6 @@ const ExtractionPage: React.FC = (props) => { } }; - const studiesDisplayed = - currentChip === EExtractionStatus.COMPLETED - ? studiesDisplayedState.completed - : currentChip === EExtractionStatus.SAVEDFORLATER - ? studiesDisplayedState.saveForLater - : studiesDisplayedState.uncategorized; - - const text = - currentChip === EExtractionStatus.COMPLETED - ? 'completed' - : currentChip === EExtractionStatus.SAVEDFORLATER - ? 'saved for later' - : 'uncategorized'; - - const pxInVh = useMemo(() => Math.round((windowHeight * 60) / 100), [windowHeight]); - const isReadyToMoveToNextStep = useMemo( () => extractionSummary.total === extractionSummary.completed && extractionSummary.total > 0, @@ -307,80 +202,9 @@ const ExtractionPage: React.FC = (props) => { - - - handleSelectChip(EExtractionStatus.UNCATEGORIZED)} - color="warning" - sx={{ marginRight: '8px' }} - variant={ - currentChip === EExtractionStatus.UNCATEGORIZED - ? 'filled' - : 'outlined' - } - icon={} - label={`Uncategorized (${studiesDisplayedState.uncategorized.length})`} - /> - handleSelectChip(EExtractionStatus.SAVEDFORLATER)} - variant={ - currentChip === EExtractionStatus.SAVEDFORLATER - ? 'filled' - : 'outlined' - } - color="info" - sx={{ marginRight: '8px' }} - icon={} - label={`Save for later (${studiesDisplayedState.saveForLater.length})`} - /> - handleSelectChip(EExtractionStatus.COMPLETED)} - variant={ - currentChip === EExtractionStatus.COMPLETED ? 'filled' : 'outlined' - } - color="success" - sx={{ marginRight: '8px' }} - icon={} - label={`Completed (${studiesDisplayedState.completed.length})`} - /> - - - - {studiesDisplayed.length} studies - - - - - {studiesDisplayed.length === 0 && ( - - No studies marked as {text} - - )} - - data.studies[index]?.id || index} - itemData={{ - studies: studiesDisplayed, - currentSelectedChip: currentChip, - canEdit: canEdit, - }} - layout="vertical" - overscanCount={3} - > - {ReadOnlyStudySummaryFixedSizeListRow} - - + + + diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.module.css b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.module.css new file mode 100644 index 00000000..7a6e3849 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.module.css @@ -0,0 +1,12 @@ +.completed { + background-color: #e5ffe5; +} + +.savedforlater { + background-color: #effbff; +} + +.uncategorized { + /* background-color: #ffffeb; */ + background-color: white; +} \ No newline at end of file diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.tsx new file mode 100644 index 00000000..f61f96cd --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.tsx @@ -0,0 +1,429 @@ +import { + Box, + Chip, + Pagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + Typography, +} from '@mui/material'; +import { + ColumnFiltersState, + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + PaginationState, + RowData, + SortingState, + useReactTable, +} from '@tanstack/react-table'; +import { useGetStudysetById } from 'hooks'; +import { IStudyExtractionStatus } from 'hooks/projects/useGetProjects'; +import { StudyReturn } from 'neurostore-typescript-sdk'; +import { + useProjectExtractionStudysetId, + useProjectExtractionStudyStatusList, + useProjectId, +} from 'pages/Project/store/ProjectStore'; +import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { EExtractionStatus } from '../ExtractionPage'; +import styles from './ExtractionTable.module.css'; +import { ExtractionTableAuthorCell, ExtractionTableAuthorHeader } from './ExtractionTableAuthor'; +import { ExtractionTableDOICell, ExtractionTableDOIHeader } from './ExtractionTableDOI'; +import ExtractionTableFilterInput from './ExtractionTableFilterInput'; +import { ExtractionTableJournalCell, ExtractionTableJournalHeader } from './ExtractionTableJournal'; +import { ExtractionTableNameCell, ExtractionTableNameHeader } from './ExtractionTableName'; +import { ExtractionTablePMIDCell, ExtractionTablePMIDHeader } from './ExtractionTablePMID'; +import { ExtractionTableStatusCell, ExtractionTableStatusHeader } from './ExtractionTableStatus'; +import { ExtractionTableYearCell, ExtractionTableYearHeader } from './ExtractionTableYear'; + +//allows us to define custom properties for our columns +declare module '@tanstack/react-table' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ColumnMeta { + filterVariant?: 'text' | 'status-select' | 'journal-autocomplete'; + } +} + +export type IExtractionTableStudy = StudyReturn & { status: EExtractionStatus | undefined }; + +const columnHelper = createColumnHelper(); + +const ExtractionTable: React.FC = () => { + const studysetId = useProjectExtractionStudysetId(); + const projectId = useProjectId(); + const navigate = useNavigate(); + const studyStatusList = useProjectExtractionStudyStatusList(); + const { data: studyset } = useGetStudysetById(studysetId, true); // this should already be loaded in the cache from the parent component + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 25, + }); + + useEffect(() => { + const state = sessionStorage.getItem(`${projectId}-extraction-table`); + if (!state) return; + + const parsedState = JSON.parse(state) as { + columnFilters: ColumnFiltersState; + sorting: SortingState; + studies: string[]; + }; + + if (parsedState.columnFilters) setColumnFilters(parsedState.columnFilters); + if (parsedState.sorting) setSorting(parsedState.sorting); + }, [projectId]); + + const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); + + const studyStatusMap = useMemo(() => { + const map = new Map(); + studyStatusList?.forEach((studyStatus) => { + map.set(studyStatus.id, studyStatus); + }); + return map; + }, [studyStatusList]); + + const data: Array = useMemo(() => { + const studies = (studyset?.studies || []) as Array; + return studies.map((study) => ({ + ...study, + status: studyStatusMap.get(study?.id || '')?.status, + })); + }, [studyStatusMap, studyset?.studies]); + + const columns = useMemo(() => { + return [ + columnHelper.accessor(({ year }) => (year ? String(year) : ''), { + id: 'year', + size: 5, + minSize: 5, + maxSize: 5, + cell: ExtractionTableYearCell, + header: ExtractionTableYearHeader, + enableSorting: true, + enableColumnFilter: true, + filterFn: 'includesString', + meta: { + filterVariant: 'text', + }, + }), + columnHelper.accessor('name', { + id: 'name', + cell: ExtractionTableNameCell, + size: 25, + minSize: 25, + maxSize: 25, + header: ExtractionTableNameHeader, + enableSorting: true, + sortingFn: 'text', + filterFn: 'includesString', + meta: { + filterVariant: 'text', + }, + }), + columnHelper.accessor('authors', { + id: 'authors', + size: 20, + minSize: 20, + maxSize: 20, + enableSorting: true, + enableColumnFilter: true, + sortingFn: 'text', + filterFn: 'includesString', + cell: ExtractionTableAuthorCell, + header: ExtractionTableAuthorHeader, + meta: { + filterVariant: 'text', + }, + }), + columnHelper.accessor('publication', { + id: 'journal', + size: 15, + minSize: 15, + maxSize: 15, + enableSorting: true, + enableColumnFilter: true, + cell: ExtractionTableJournalCell, + header: ExtractionTableJournalHeader, + meta: { + filterVariant: 'journal-autocomplete', + }, + }), + columnHelper.accessor('doi', { + id: 'doi', + size: 15, + minSize: 15, + maxSize: 15, + sortingFn: 'alphanumeric', + enableSorting: true, + enableColumnFilter: true, + filterFn: 'includesString', + cell: ExtractionTableDOICell, + header: ExtractionTableDOIHeader, + meta: { + filterVariant: 'text', + }, + }), + columnHelper.accessor('pmid', { + id: 'pmid', + size: 10, + minSize: 10, + maxSize: 10, + enableColumnFilter: true, + filterFn: 'includesString', + cell: ExtractionTablePMIDCell, + header: ExtractionTablePMIDHeader, + enableSorting: true, + sortingFn: 'alphanumeric', + meta: { + filterVariant: 'text', + }, + }), + columnHelper.accessor('status', { + id: 'status', + size: 10, + minSize: 10, + maxSize: 10, + enableSorting: true, + cell: ExtractionTableStatusCell, + filterFn: (row, columnId, filterValue: EExtractionStatus | null) => { + if (filterValue === null) return true; + const studyStatus = row.getValue(columnId) as EExtractionStatus | undefined; + + // uncategorized can be undefined or it can be "uncategorized" + if (filterValue === EExtractionStatus.UNCATEGORIZED) { + return studyStatus === filterValue || studyStatus === undefined; + } + + return studyStatus === filterValue; + }, + header: ExtractionTableStatusHeader, + enableColumnFilter: true, + meta: { + filterVariant: 'status-select', + }, + }), + ]; + }, []); + + const table = useReactTable({ + data: data, + columns: columns, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnFiltersChange: setColumnFilters, + state: { + pagination: pagination, + columnFilters: columnFilters, + sorting: sorting, + }, + meta: { + studyStatusMap, + }, + }); + + const handleRowsPerPageChange = useCallback( + (event: ChangeEvent) => { + const newRowsPerPage = parseInt(event.target.value); + if (!isNaN(newRowsPerPage)) setPagination({ pageIndex: 0, pageSize: newRowsPerPage }); + }, + [] + ); + + const handlePaginationChange = useCallback((_event: any, page: number) => { + // page is 0 indexed + setPagination((prev) => ({ + ...prev, + pageIndex: page, + })); + }, []); + + // the two pagination functionds act differently so we need to assume different things for each + const handlePaginationChangeMuiPaginator = useCallback((_event: any, page: number) => { + // page is 0 indexed + setPagination((prev) => ({ + ...prev, + pageIndex: page - 1, + })); + }, []); + + return ( + + + + Total: {data.length} studies + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getCanFilter() ? ( + + ) : ( + + )} + + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + { + if (!row.original.id) return; + sessionStorage.setItem( + `${projectId}-extraction-table`, + JSON.stringify({ + columnFilters: table.getState().columnFilters, + sorting: table.getState().sorting, + studies: table + .getSortedRowModel() + .rows.map((r) => r.original.id), + }) + ); + navigate( + `/projects/${projectId}/extraction/studies/${row.original.id}/edit` + ); + }} + sx={{ + '&:hover': { filter: 'brightness(0.9)', cursor: 'pointer' }, + }} + > + {row.getVisibleCells().map((cell) => ( + + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + + ))} + + ))} + +
+
+ + + + + {columnFilters + .filter((filter) => !!filter.value) + .map((filter) => ( + + table.setColumnFilters((prev) => + prev.filter((f) => f.id !== filter.id) + ) + } + key={filter.id} + color="primary" + variant="outlined" + sx={{ margin: '1px', fontSize: '12px', maxWidth: '200px' }} + label={`Filtering ${filter.id.toUpperCase()}: ${filter.value}`} + size="small" + /> + ))} + {sorting.map((sort) => ( + { + table.setSorting((prev) => + prev.filter((f) => f.id !== sort.id) + ); + }} + color="secondary" + variant="outlined" + sx={{ margin: '1px', fontSize: '12px', maxWidth: '200px' }} + label={`Sorting by ${sort.id.toUpperCase()}: ${ + sort.desc ? 'desc' : 'asc' + }`} + size="small" + /> + ))} + + + + Viewing {table.getFilteredRowModel().rows.length} / {data.length} + + + + +
+ ); +}; + +export default ExtractionTable; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableAuthor.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableAuthor.tsx new file mode 100644 index 00000000..117ab4ed --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableAuthor.tsx @@ -0,0 +1,59 @@ +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTableAuthorCell: React.FC> = ( + props +) => { + const value = props.getValue(); + return {value}; +}; + +export const ExtractionTableAuthorHeader: React.FC< + HeaderContext +> = ({ table, column }) => { + const isSorted = column.getIsSorted(); + + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'authors', desc: true }]); + } + }} + > + Authors + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'authors', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'authors', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableDOI.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableDOI.tsx new file mode 100644 index 00000000..f4a21dd4 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableDOI.tsx @@ -0,0 +1,63 @@ +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTableDOICell: React.FC> = ( + props +) => { + const value = props.getValue(); + return ( + + {value} + + ); +}; + +export const ExtractionTableDOIHeader: React.FC> = ({ + table, + column, +}) => { + const isSorted = column.getIsSorted(); + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'doi', desc: true }]); + } + }} + > + DOI + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'doi', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'doi', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableFilterInput.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableFilterInput.tsx new file mode 100644 index 00000000..c9ac8c29 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableFilterInput.tsx @@ -0,0 +1,52 @@ +import { Box } from '@mui/material'; +import { Column } from '@tanstack/react-table'; +import DebouncedTextField from 'components/DebouncedTextField'; +import { useCallback } from 'react'; +import { EExtractionStatus } from '../ExtractionPage'; +import { IExtractionTableStudy } from './ExtractionTable'; +import ExtractionTableJournalAutocomplete from './ExtractionTableJournalAutocomplete'; +import ExtractionTableStatusFilter from './ExtractionTableStatusFilter'; + +const ExtractionTableFilterInput: React.FC<{ column: Column }> = ({ + column, +}) => { + const columnFilterValue = column.getFilterValue(); + const { filterVariant } = column.columnDef.meta ?? {}; + + const handleChangeAutocomplete = useCallback( + (event: string | null | undefined) => { + column.setFilterValue(event ?? null); + }, + [column] + ); + + if (filterVariant === 'status-select') { + return ( + + ); + } else if (filterVariant === 'journal-autocomplete') { + return ( + + ); + } else { + return ( + + + + ); + } +}; + +export default ExtractionTableFilterInput; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournal.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournal.tsx new file mode 100644 index 00000000..d76bc065 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournal.tsx @@ -0,0 +1,58 @@ +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTableJournalCell: React.FC> = ( + props +) => { + const value = props.getValue(); + return {value}; +}; + +export const ExtractionTableJournalHeader: React.FC< + HeaderContext +> = ({ table, column }) => { + const isSorted = column.getIsSorted(); + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'journal', desc: true }]); + } + }} + > + Journal + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'journal', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'journal', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournalAutocomplete.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournalAutocomplete.tsx new file mode 100644 index 00000000..07225d68 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournalAutocomplete.tsx @@ -0,0 +1,43 @@ +import { Autocomplete, Box, TextField } from '@mui/material'; +import { useGetStudysetById } from 'hooks'; +import { StudyReturn } from 'neurostore-typescript-sdk'; +import { useProjectExtractionStudysetId } from 'pages/Project/store/ProjectStore'; +import { useMemo } from 'react'; + +const ExtractionTableJournalAutocomplete: React.FC<{ + value: string; + onChange: (value: string | null) => void; +}> = ({ value, onChange }) => { + const studysetId = useProjectExtractionStudysetId(); + const { data: studyset } = useGetStudysetById(studysetId, true); + + const options = useMemo(() => { + if (!studyset) return []; + const journalsSet = new Set(); + (studyset.studies || []).forEach((study) => { + if ((study as StudyReturn)?.publication) { + journalsSet.add((study as StudyReturn)?.publication || ''); + } + }); + + return Array.from(journalsSet).sort(); + }, [studyset]); + + const handleChange = (event: any, value: string | null) => { + onChange(value); + }; + + return ( + + } + onChange={handleChange} + value={value || null} + options={options} + /> + + ); +}; + +export default ExtractionTableJournalAutocomplete; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableName.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableName.tsx new file mode 100644 index 00000000..66f9820e --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableName.tsx @@ -0,0 +1,63 @@ +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTableNameCell: React.FC> = ( + props +) => { + const value = props.getValue(); + return ( + + {value} + + ); +}; + +export const ExtractionTableNameHeader: React.FC> = ({ + table, + column, +}) => { + const isSorted = column.getIsSorted(); + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'name', desc: true }]); + } + }} + > + Name + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'name', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'name', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTablePMID.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTablePMID.tsx new file mode 100644 index 00000000..8cdcb882 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTablePMID.tsx @@ -0,0 +1,59 @@ +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTablePMIDCell: React.FC> = ( + props +) => { + const value = props.getValue(); + return {value}; +}; + +export const ExtractionTablePMIDHeader: React.FC> = ({ + column, + table, +}) => { + const isSorted = column.getIsSorted(); + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'pmid', desc: true }]); + } + }} + > + PMID + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'pmid', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'pmid', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatus.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatus.tsx new file mode 100644 index 00000000..c8696575 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatus.tsx @@ -0,0 +1,118 @@ +import { ArrowDownward, CheckCircle, QuestionMark } from '@mui/icons-material'; +import BookmarkIcon from '@mui/icons-material/Bookmark'; +import { Box, Button, ButtonGroup, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { useProjectExtractionAddOrUpdateStudyListStatus } from 'pages/Project/store/ProjectStore'; +import { EExtractionStatus } from '../ExtractionPage'; +import { IExtractionTableStudy } from './ExtractionTable'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; + +export const ExtractionTableStatusCell: React.FC< + CellContext +> = (props) => { + const status = props.getValue(); + + const updateStudyListStatus = useProjectExtractionAddOrUpdateStudyListStatus(); + + return ( + + + + + + + + ); +}; + +export const ExtractionTableStatusHeader: React.FC< + HeaderContext +> = ({ table, column }) => { + const isSorted = column.getIsSorted(); + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'status', desc: true }]); + } + }} + > + Status + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'status', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'status', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatusFilter.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatusFilter.tsx new file mode 100644 index 00000000..7bf56c55 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatusFilter.tsx @@ -0,0 +1,91 @@ +import { Bookmark, CheckCircle, QuestionMark } from '@mui/icons-material'; +import { + Box, + ListItemIcon, + ListItemText, + MenuItem, + Select, + SelectChangeEvent, + Typography, +} from '@mui/material'; +import { EExtractionStatus } from '../ExtractionPage'; + +const ExtractionStatusInput: React.FC = (props) => { + switch (props) { + case EExtractionStatus.COMPLETED: + return ( + + + Completed + + ); + case EExtractionStatus.SAVEDFORLATER: + return ( + + + Saved for Later + + ); + case EExtractionStatus.UNCATEGORIZED: + return ( + + + Unreviewed + + ); + case undefined: + default: + return ( + + All + + ); + } +}; + +const ExtractionTableStatusFilter: React.FC<{ + value: EExtractionStatus | null; + onChange: (val: string | null) => void; +}> = ({ value, onChange }) => { + const handleOnChange = (event: SelectChangeEvent) => { + onChange(event.target.value ? event.target.value : null); + }; + + return ( + + + + ); +}; + +export default ExtractionTableStatusFilter; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableYear.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableYear.tsx new file mode 100644 index 00000000..5d48a275 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableYear.tsx @@ -0,0 +1,60 @@ +import { ArrowDownward } from '@mui/icons-material'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import { Box, IconButton, Link, Tooltip, Typography } from '@mui/material'; +import { CellContext, HeaderContext } from '@tanstack/react-table'; +import { IExtractionTableStudy } from './ExtractionTable'; + +export const ExtractionTableYearCell: React.FC> = ( + props +) => { + const value = props.getValue(); + return {value}; +}; + +export const ExtractionTableYearHeader: React.FC> = ({ + table, + column, +}) => { + const isSorted = column.getIsSorted(); + + return ( + + + { + if (!!isSorted) { + table.resetSorting(); + } else { + table.setSorting([{ id: 'year', desc: true }]); + } + }} + > + Year + + + {!!isSorted && ( + <> + {isSorted === 'asc' ? ( + table.setSorting([{ id: 'year', desc: true }])} + > + + + ) : ( + table.setSorting([{ id: 'year', desc: false }])} + > + + + )} + + )} + + ); +}; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.styles.ts b/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.styles.ts index edf112d1..dec88b2a 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.styles.ts +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.styles.ts @@ -4,14 +4,6 @@ const StudyListItemStyles: Style = { listItem: { display: 'flex', padding: '10px', - width: 'calc(100% - 20px)', - height: 'calc(100% - 20px)', - transition: '0.1s ease-in-out', - ':hover': { - backgroundColor: '#efefef', - borderRadius: '8px', - cursor: 'pointer', - }, }, }; diff --git a/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.tsx b/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.tsx index bc02d3ea..35d93070 100644 --- a/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.tsx +++ b/compose/neurosynth-frontend/src/pages/Extraction/components/ReadOnlyStudySummary.tsx @@ -13,7 +13,7 @@ import useUserCanEdit from 'hooks/useUserCanEdit'; const ReadOnlyStudySummaryVirtualizedItem: React.FC< StudyReturn & { - currentSelectedChip: EExtractionStatus; + currentStatus: EExtractionStatus; canEdit: boolean; style: React.CSSProperties; } @@ -39,16 +39,19 @@ const ReadOnlyStudySummaryVirtualizedItem: React.FC< }; const showMarkAsCompleteButton = - props.currentSelectedChip === EExtractionStatus.UNCATEGORIZED || - props.currentSelectedChip === EExtractionStatus.SAVEDFORLATER; + props.currentStatus === EExtractionStatus.UNCATEGORIZED || + props.currentStatus === EExtractionStatus.SAVEDFORLATER; const showMarkAsSaveForLaterButton = - props.currentSelectedChip === EExtractionStatus.UNCATEGORIZED || - props.currentSelectedChip === EExtractionStatus.COMPLETED; + props.currentStatus === EExtractionStatus.UNCATEGORIZED || + props.currentStatus === EExtractionStatus.COMPLETED; return ( - - + + {`${props.year ? `(${props.year}) ` : ''}${props.name}`} diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx index 77058652..f05a3bf3 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyAnnotationsHotTable.tsx @@ -1,7 +1,7 @@ import { HotTable } from '@handsontable/react'; import { Box } from '@mui/material'; import { CellChange, ChangeSource, RangeType } from 'handsontable/common'; -import { useRef } from 'react'; +import { useMemo, useRef } from 'react'; import { useAnnotationNoteKeys, useUpdateAnnotationNotes } from 'stores/AnnotationStore.actions'; import { sanitizePaste } from 'components/HotTables/HotTables.utils'; import useEditStudyAnnotationsHotTable from 'pages/Study/components/useEditStudyAnnotationsHotTable'; @@ -44,6 +44,10 @@ const EditStudyAnnotationsHotTable: React.FC<{ readonly?: boolean }> = ({ readon return true; }; + const memoizedData = useMemo(() => { + return JSON.parse(JSON.stringify(data || [])); + }, [data]); + return ( = ({ readon colWidths={colWidths} columns={columns} colHeaders={colHeaders} - data={JSON.parse(JSON.stringify(data || []))} + data={memoizedData} ref={hotTableRef} /> diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.spec.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.spec.tsx index a63d069b..43370157 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.spec.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.spec.tsx @@ -1,16 +1,15 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useGetExtractionSummary, useGetStudysetById } from 'hooks'; -import { IStudyExtractionStatus } from 'hooks/projects/useGetProjects'; +import { useGetExtractionSummary, useGetStudysetById, useUserCanEdit } from 'hooks'; import { EExtractionStatus } from 'pages/Extraction/ExtractionPage'; import { useProjectExtractionAddOrUpdateStudyListStatus, - useProjectExtractionStudyStatusList, + useProjectExtractionStudysetId, + useProjectId, } from 'pages/Project/store/ProjectStore'; import { useStudyId } from 'pages/Study/store/StudyStore'; import { useNavigate } from 'react-router-dom'; import EditStudyToolbar from './EditStudyToolbar'; -import { useUserCanEdit } from 'hooks'; jest.mock('hooks'); jest.mock('react-router-dom'); @@ -18,6 +17,11 @@ jest.mock('pages/Project/store/ProjectStore.ts'); jest.mock('pages/Study/store/StudyStore.ts'); describe('EditStudyToolbar Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + window.sessionStorage.clear(); + }); + it('should render', () => { render(); }); @@ -80,67 +84,60 @@ describe('EditStudyToolbar Component', () => { ); }); - it('should move to previous study', () => { + it('should move to previous study when receiving state from the table', () => { // ARRANGE - Storage.prototype.getItem = jest.fn().mockReturnValue(EExtractionStatus.SAVEDFORLATER); // mock localStorage + window.sessionStorage.setItem( + `projectid-extraction-table`, + JSON.stringify({ + columnFilters: [], + sorting: [], + studies: ['study-1', 'study-2', 'study-3', 'study-4'], + }) + ); (useStudyId as jest.Mock).mockReturnValue('study-2'); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); (useUserCanEdit as jest.Mock).mockReturnValue(true); + + render(); + // ACT + userEvent.click(screen.getByTestId('ArrowBackIcon')); + // ASSERT + expect(useNavigate()).toHaveBeenCalledWith( + '/projects/projectid/extraction/studies/study-1/edit' + ); + }); + + it('should move to previous study if there is no state received from the table', () => { + // ARRANGE + (useStudyId as jest.Mock).mockReturnValue('study-3'); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); + (useUserCanEdit as jest.Mock).mockReturnValue(true); + useGetStudysetById().data = { - studies: [{ id: 'study-0' }, { id: 'study-0.5' }, { id: 'study-2' }, { id: 'study-3' }], + studies: [{ id: 'study-2' }, { id: 'study-3' }, { id: 'study-4' }], }; - (useProjectExtractionStudyStatusList as jest.Mock).mockReturnValue([ - { - id: 'study-0', - status: EExtractionStatus.SAVEDFORLATER, - }, - { - id: 'study-0.5', - status: EExtractionStatus.UNCATEGORIZED, - }, - { - id: 'study-1', - status: EExtractionStatus.COMPLETED, - }, - { - id: 'study-2', - status: EExtractionStatus.SAVEDFORLATER, - }, - { - id: 'study-3', - status: EExtractionStatus.COMPLETED, - }, - ] as IStudyExtractionStatus[]); render(); // ACT userEvent.click(screen.getByTestId('ArrowBackIcon')); // ASSERT expect(useNavigate()).toHaveBeenCalledWith( - '/projects/project-id/extraction/studies/study-0/edit' + '/projects/projectid/extraction/studies/study-2/edit' ); }); - it('should disable if no previous study', () => { + it('should disable the back button if on the first study', () => { // ARRANGE - Storage.prototype.getItem = jest.fn().mockReturnValue(EExtractionStatus.SAVEDFORLATER); // mock localStorage (useStudyId as jest.Mock).mockReturnValue('study-2'); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); + (useUserCanEdit as jest.Mock).mockReturnValue(true); + useGetStudysetById().data = { - studies: [{ id: 'study-1' }, { id: 'study-2' }, { id: 'study-3' }], + studies: [{ id: 'study-2' }, { id: 'study-3' }, { id: 'study-4' }], }; - (useProjectExtractionStudyStatusList as jest.Mock).mockReturnValue([ - { - id: 'study-1', - status: EExtractionStatus.UNCATEGORIZED, - }, - { - id: 'study-2', - status: EExtractionStatus.SAVEDFORLATER, - }, - { - id: 'study-3', - status: EExtractionStatus.COMPLETED, - }, - ] as IStudyExtractionStatus[]); render(); // ACT @@ -149,77 +146,69 @@ describe('EditStudyToolbar Component', () => { expect(arrowBackIcon).toBeDisabled(); }); - it('should move to next study', () => { + it('should move to the next study when receiving state from the table', () => { // ARRANGE - Storage.prototype.getItem = jest.fn().mockReturnValue(EExtractionStatus.COMPLETED); // mock localStorage + window.sessionStorage.setItem( + `projectid-extraction-table`, + JSON.stringify({ + columnFilters: [], + sorting: [], + studies: ['study-1', 'study-2', 'study-3', 'study-4'], + }) + ); (useStudyId as jest.Mock).mockReturnValue('study-2'); - useGetStudysetById().data = { - studies: [ - { id: 'study-1' }, - { id: 'study-2' }, - { id: 'study-3' }, - { id: 'study-4' }, - { id: 'study-5' }, - ], - }; - (useProjectExtractionStudyStatusList as jest.Mock).mockReturnValue([ - { - id: 'study-1', - status: EExtractionStatus.UNCATEGORIZED, - }, - { - id: 'study-2', - status: EExtractionStatus.COMPLETED, - }, - { - id: 'study-3', - status: EExtractionStatus.SAVEDFORLATER, - }, - { - id: 'study-4', - status: EExtractionStatus.UNCATEGORIZED, - }, - { - id: 'study-5', - status: EExtractionStatus.COMPLETED, - }, - ] as IStudyExtractionStatus[]); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); + (useUserCanEdit as jest.Mock).mockReturnValue(true); render(); // ACT userEvent.click(screen.getByTestId('ArrowForwardIcon')); // ASSERT expect(useNavigate()).toHaveBeenCalledWith( - '/projects/project-id/extraction/studies/study-5/edit' + '/projects/projectid/extraction/studies/study-3/edit' ); }); - it('should disable if no next study', () => { + it('should move to the next study if there is no state received from the table', () => { // ARRANGE - Storage.prototype.getItem = jest.fn().mockReturnValue(EExtractionStatus.SAVEDFORLATER); // mock localStorage - (useStudyId as jest.Mock).mockReturnValue('study-2'); + (useStudyId as jest.Mock).mockReturnValue('study-3'); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); + (useUserCanEdit as jest.Mock).mockReturnValue(true); + useGetStudysetById().data = { - studies: [{ id: 'study-1' }, { id: 'study-2' }, { id: 'study-3' }], + studies: [{ id: 'study-2' }, { id: 'study-3' }, { id: 'study-4' }], }; - (useProjectExtractionStudyStatusList as jest.Mock).mockReturnValue([ - { - id: 'study-1', - status: EExtractionStatus.UNCATEGORIZED, - }, - { - id: 'study-2', - status: EExtractionStatus.SAVEDFORLATER, - }, - { - id: 'study-3', - status: EExtractionStatus.UNCATEGORIZED, - }, - ] as IStudyExtractionStatus[]); render(); // ACT - const arrowBackIcon = screen.getByTestId('ArrowForwardIcon').parentElement; + userEvent.click(screen.getByTestId('ArrowForwardIcon')); // ASSERT - expect(arrowBackIcon).toBeDisabled(); + expect(useNavigate()).toHaveBeenCalledWith( + '/projects/projectid/extraction/studies/study-4/edit' + ); + }); + + it('should disable the next button if on the last study', () => { + // ARRANGE + window.sessionStorage.setItem( + `projectid-extraction-table`, + JSON.stringify({ + columnFilters: [], + sorting: [], + studies: ['study-1', 'study-2', 'study-3', 'study-4'], + }) + ); + (useStudyId as jest.Mock).mockReturnValue('study-4'); + (useProjectExtractionStudysetId as jest.Mock).mockReturnValue('studysetid'); + (useProjectId as jest.Mock).mockReturnValue('projectid'); + (useUserCanEdit as jest.Mock).mockReturnValue(true); + + render(); + // ACT + const arrowForwardIcon = screen.getByTestId('ArrowForwardIcon').parentElement; + // ASSERT + expect(arrowForwardIcon).toBeDisabled(); }); }); diff --git a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx index d2f2fd7c..9ea015a5 100644 --- a/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx +++ b/compose/neurosynth-frontend/src/pages/Study/components/EditStudyToolbar.tsx @@ -3,62 +3,75 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import BookmarkIcon from '@mui/icons-material/Bookmark'; import CheckIcon from '@mui/icons-material/Check'; import DoneAllIcon from '@mui/icons-material/DoneAll'; -import { Box, CircularProgress, IconButton, Tooltip } from '@mui/material'; -import { useGetExtractionSummary, useGetStudysetById } from 'hooks'; +import { Box, CircularProgress, IconButton, Tooltip, Typography } from '@mui/material'; +import { ColumnFiltersState, SortingState } from '@tanstack/react-table'; +import ProgressLoader from 'components/ProgressLoader'; +import GlobalStyles from 'global.styles'; +import { useGetExtractionSummary, useGetStudysetById, useUserCanEdit } from 'hooks'; import { StudyReturn } from 'neurostore-typescript-sdk'; import { EExtractionStatus } from 'pages/Extraction/ExtractionPage'; +import { IProjectPageLocationState } from 'pages/Project/ProjectPage'; import { useProjectExtractionAddOrUpdateStudyListStatus, - useProjectExtractionStudyStatus, - useProjectExtractionStudyStatusList, useProjectExtractionStudysetId, + useProjectExtractionStudyStatus, useProjectId, useProjectMetaAnalysisCanEdit, useProjectUser, } from 'pages/Project/store/ProjectStore'; import { useStudyId } from 'pages/Study/store/StudyStore'; -import { useCallback, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; 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) { - case EExtractionStatus.UNCATEGORIZED: - return 'uncategorized'; - case EExtractionStatus.COMPLETED: - return 'completed'; - case EExtractionStatus.SAVEDFORLATER: - return 'saved for later'; - default: - return 'uncategorized'; - } -}; - -const getCurrSelectedChip = (projectId: string | undefined) => { - return ( - (localStorage.getItem(`SELECTED_CHIP-${projectId}`) as EExtractionStatus) || - EExtractionStatus.UNCATEGORIZED - ); -}; const EditStudyToolbar: React.FC<{ isViewOnly?: boolean }> = ({ isViewOnly = false }) => { + const navigate = useNavigate(); + const canEditMetaAnalyses = useProjectMetaAnalysisCanEdit(); + const projectId = useProjectId(); + const extractionSummary = useGetExtractionSummary(projectId || ''); + const studyId = useStudyId(); const extractionStatus = useProjectExtractionStudyStatus(studyId || ''); - const extractionSummary = useGetExtractionSummary(projectId || ''); - const studysetId = useProjectExtractionStudysetId(); - // nested msut be true so that we maintain to alphabetical study order - // if nested is false, we do not have access to study names and so will be given study Ids in random order - const { data: studyset } = useGetStudysetById(studysetId, true); - const studyStatusList = useProjectExtractionStudyStatusList(); - const navigate = useNavigate(); - const canEditMetaAnalyses = useProjectMetaAnalysisCanEdit(); + const user = useProjectUser(); const canEdit = useUserCanEdit(user ?? undefined); + const studysetId = useProjectExtractionStudysetId(); + const { data, isLoading, isError } = useGetStudysetById(studysetId || '', true); + + // derived from the extraction table + const [studiesState, setStudiesState] = useState<{ + columnFilters: ColumnFiltersState; + sorting: SortingState; + studies: string[]; + }>({ + columnFilters: [], + sorting: [], + studies: [], + }); + + useEffect(() => { + const stateFromSessionStorage = sessionStorage.getItem(`${projectId}-extraction-table`); + if (!stateFromSessionStorage) { + setStudiesState((prev) => ({ + ...prev, + studies: (data?.studies || []).map((study) => (study as StudyReturn).id as string), + })); + } else { + try { + const parsedState = JSON.parse(stateFromSessionStorage) as { + columnFilters: ColumnFiltersState; + sorting: SortingState; + studies: string[]; + }; + setStudiesState(parsedState); + } catch (e) { + throw new Error('couldnt parse table state from session storage'); + } + } + }, [data?.studies, projectId]); + const updateStudyListStatus = useProjectExtractionAddOrUpdateStudyListStatus(); const handleClickStudyListStatus = (status: EExtractionStatus) => { @@ -67,74 +80,22 @@ const EditStudyToolbar: React.FC<{ isViewOnly?: boolean }> = ({ isViewOnly = fal } }; - const getValidPrevStudyId = useCallback((): string | undefined => { - if (!studyset?.studies) return undefined; - - const CURR_SELECTED_CHIP_STATUS = getCurrSelectedChip(projectId); - const currStudyIndex = (studyset.studies || []).findIndex( - (study) => (study as StudyReturn)?.id === studyId - ); - if (currStudyIndex < 0) return undefined; - const map = new Map(); - studyStatusList.forEach((studyStatus) => { - map.set(studyStatus.id, studyStatus.status); - }); - - // go through all previous studies to find the next one before this current selected study that has the current selected chip status. - // This will also take care of the case where the current study selected is the first one - for (let i = currStudyIndex - 1; i >= 0; i--) { - const aStudy = studyset.studies[i] as StudyReturn; - if (!aStudy?.id) return undefined; - const aStudyStatus = map.get(aStudy.id) || EExtractionStatus.UNCATEGORIZED; - - if (aStudyStatus === CURR_SELECTED_CHIP_STATUS) return aStudy.id; - } - return undefined; - }, [projectId, studyId, studyStatusList, studyset]); - - const getValidNextStudyId = useCallback((): string | undefined => { - if (!studyset?.studies) return undefined; - - const CURR_SELECTED_CHIP_STATUS = getCurrSelectedChip(projectId); - const currStudyIndex = (studyset.studies || []).findIndex( - (study) => (study as StudyReturn)?.id === studyId - ); - if (currStudyIndex < 0) return undefined; - const map = new Map(); - studyStatusList.forEach((studyStatus) => { - map.set(studyStatus.id, studyStatus.status); - }); - - for (let i = currStudyIndex + 1; i <= studyset.studies.length; i++) { - const aStudy = studyset.studies[i] as StudyReturn; - if (!aStudy?.id) return undefined; - const aStudyStatus = map.get(aStudy.id) || EExtractionStatus.UNCATEGORIZED; - - if (aStudyStatus === CURR_SELECTED_CHIP_STATUS) return aStudy.id; - } - return undefined; - }, [projectId, studyId, studyStatusList, studyset]); - const handleMoveToPreviousStudy = () => { - const prevId = getValidPrevStudyId(); - if (prevId) { - canEdit - ? navigate(`/projects/${projectId}/extraction/studies/${prevId}/edit`) - : navigate(`/projects/${projectId}/extraction/studies/${prevId}`); - } else { - throw new Error('no studies before this one'); - } + const index = studiesState.studies.indexOf(studyId || ''); + const prevId = studiesState.studies[index - 1]; + if (!prevId) throw new Error('no previous study'); + canEdit + ? navigate(`/projects/${projectId}/extraction/studies/${prevId}/edit`) + : navigate(`/projects/${projectId}/extraction/studies/${prevId}`); }; const handleMoveToNextStudy = () => { - const nextId = getValidNextStudyId(); - if (nextId) { - canEdit - ? navigate(`/projects/${projectId}/extraction/studies/${nextId}/edit`) - : navigate(`/projects/${projectId}/extraction/studies/${nextId}`); - } else { - throw new Error('no studies after this one'); - } + const index = studiesState.studies.indexOf(studyId || ''); + const nextId = studiesState.studies[index + 1]; + if (!nextId) throw new Error('no next study'); + canEdit + ? navigate(`/projects/${projectId}/extraction/studies/${nextId}/edit`) + : navigate(`/projects/${projectId}/extraction/studies/${nextId}`); }; const handleContinueToMetaAnalysisCreation = () => { @@ -169,33 +130,16 @@ const EditStudyToolbar: React.FC<{ isViewOnly?: boolean }> = ({ isViewOnly = fal }, [extractionSummary.completed, extractionSummary.total]); const hasPrevStudies = useMemo(() => { - return getValidPrevStudyId() !== undefined; - }, [getValidPrevStudyId]); + const studies = studiesState.studies; + const index = studies.indexOf(studyId || ''); + return index - 1 >= 0; + }, [studiesState.studies, studyId]); const hasNextStudies = useMemo(() => { - return getValidNextStudyId() !== undefined; - }, [getValidNextStudyId]); - - const currSelectedChipText = useMemo(() => { - const currSelectedChip = (localStorage.getItem(`SELECTED_CHIP-${projectId}`) || - EExtractionStatus.UNCATEGORIZED) as EExtractionStatus; - return getCurrSelectedChipText(currSelectedChip); - }, [projectId]); - - const prevNextArrowColor = useMemo(() => { - const currSelectedChip = (localStorage.getItem(`SELECTED_CHIP-${projectId}`) || - EExtractionStatus.UNCATEGORIZED) as EExtractionStatus; - switch (currSelectedChip) { - case EExtractionStatus.UNCATEGORIZED: - return 'warning.main'; - case EExtractionStatus.SAVEDFORLATER: - return 'info.main'; - case EExtractionStatus.COMPLETED: - return 'success.main'; - default: - return 'warning.main'; - } - }, [projectId]); + const studies = studiesState.studies; + const index = studies.indexOf(studyId || ''); + return index + 1 < studies.length; + }, [studiesState.studies, studyId]); return ( @@ -285,58 +229,65 @@ const EditStudyToolbar: React.FC<{ isViewOnly?: boolean }> = ({ isViewOnly = fal )} - - - {/* tooltip cannot act on a disabled element so we need to add a span here */} - - + + + ) : isError ? ( + + There was an error + + ) : ( + <> + + - - - - - - - - {/* tooltip cannot act on a disabled element so we need to add a span here */} - - + + + + + + + + - - - - - + {/* tooltip cannot act on a disabled element so we need to add a span here */} + + + + + + + + + )} diff --git a/compose/neurosynth-frontend/src/pages/Study/store/__mocks__/StudyStore.ts b/compose/neurosynth-frontend/src/pages/Study/store/__mocks__/StudyStore.ts index a02df865..ebec882a 100644 --- a/compose/neurosynth-frontend/src/pages/Study/store/__mocks__/StudyStore.ts +++ b/compose/neurosynth-frontend/src/pages/Study/store/__mocks__/StudyStore.ts @@ -2,4 +2,6 @@ const useStudyId = jest.fn().mockReturnValue('study-id'); const useStudyName = jest.fn().mockResolvedValue('test-study-name'); -export { useStudyId, useStudyName }; +const useProjectId = jest.fn().mockReturnValue('project-id'); + +export { useStudyId, useStudyName, useProjectId }; diff --git a/compose/neurosynth-frontend/tsconfig.json b/compose/neurosynth-frontend/tsconfig.json index b81f5299..46916174 100644 --- a/compose/neurosynth-frontend/tsconfig.json +++ b/compose/neurosynth-frontend/tsconfig.json @@ -18,5 +18,5 @@ "jsx": "react-jsx", "types": ["cypress", "node", "jest"] }, - "include": ["src"] + "include": ["src", "cypress"] }