From 499a3cb7cfc8e559a93119959c2c658730318d3a Mon Sep 17 00:00:00 2001 From: James Kent Date: Thu, 19 Sep 2024 14:02:37 -0500 Subject: [PATCH] Revert "Revert "[FEAT] 518 extraction phase can we make the list of studies more spreadsheet like (#820)"" This reverts commit f64bb8c3b4c8478c0d51c5b8efc5963fddf94109. --- .../cypress/e2e/pages/BaseStudyPage.cy.tsx | 2 +- .../cypress/e2e/pages/EditStudyPage.cy.tsx | 2 +- .../cypress/e2e/pages/LandingPage.cy.tsx | 2 +- .../cypress/e2e/pages/MetaAnalysisPage.cy.tsx | 2 +- .../e2e/pages/ProjectMetaAnalysesPage.cy.tsx | 2 +- .../cypress/e2e/pages/ProjectPage.cy.tsx | 2 +- .../e2e/pages/PublicStudiesPage.cy.tsx | 2 +- .../Curation/ImportStudiesDialog.cy.tsx | 12 +- .../Extraction/ExtractionTable.cy.tsx | 693 +++++++++++ .../CreateSpecificationDialog.cy.tsx | 2 +- .../SleuthImport/DoSleuthImport.cy.tsx | 2 +- .../e2e/workflows/ingestion/Ingestion.cy.tsx | 2 +- .../cypress/fixtures/Extraction/project.json | 184 +++ .../cypress/fixtures/studyset.json | 1084 ++++++++--------- compose/neurosynth-frontend/package-lock.json | 45 + compose/neurosynth-frontend/package.json | 1 + .../src/components/DebouncedTextField.tsx | 37 + .../components/Dialogs/ConfirmationDialog.tsx | 3 +- .../src/components/Search/SearchContainer.tsx | 2 +- .../src/pages/Extraction/ExtractionPage.tsx | 186 +-- .../components/ExtractionTable.module.css | 12 + .../Extraction/components/ExtractionTable.tsx | 429 +++++++ .../components/ExtractionTableAuthor.tsx | 59 + .../components/ExtractionTableDOI.tsx | 63 + .../components/ExtractionTableFilterInput.tsx | 52 + .../components/ExtractionTableJournal.tsx | 58 + .../ExtractionTableJournalAutocomplete.tsx | 43 + .../components/ExtractionTableName.tsx | 63 + .../components/ExtractionTablePMID.tsx | 59 + .../components/ExtractionTableStatus.tsx | 118 ++ .../ExtractionTableStatusFilter.tsx | 91 ++ .../components/ExtractionTableYear.tsx | 60 + .../components/ReadOnlyStudySummary.styles.ts | 8 - .../components/ReadOnlyStudySummary.tsx | 17 +- .../EditStudyAnnotationsHotTable.tsx | 8 +- .../components/EditStudyToolbar.spec.tsx | 191 ++- .../Study/components/EditStudyToolbar.tsx | 301 ++--- .../pages/Study/store/__mocks__/StudyStore.ts | 4 +- compose/neurosynth-frontend/tsconfig.json | 2 +- 39 files changed, 2842 insertions(+), 1063 deletions(-) create mode 100644 compose/neurosynth-frontend/cypress/e2e/workflows/Extraction/ExtractionTable.cy.tsx create mode 100644 compose/neurosynth-frontend/cypress/fixtures/Extraction/project.json create mode 100644 compose/neurosynth-frontend/src/components/DebouncedTextField.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.module.css create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTable.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableAuthor.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableDOI.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableFilterInput.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournal.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableJournalAutocomplete.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableName.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTablePMID.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatus.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableStatusFilter.tsx create mode 100644 compose/neurosynth-frontend/src/pages/Extraction/components/ExtractionTableYear.tsx diff --git a/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx b/compose/neurosynth-frontend/cypress/e2e/pages/BaseStudyPage.cy.tsx index 7c2b320d0..bd1a7dfe7 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 cbe97e57f..586d7573d 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 06a083d04..cd6307abc 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 4b0f70bdc..7c045940a 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 e9cdb41bb..a84f002d7 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 ebd266a09..3631c4342 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 0cf70328f..dc4a824d4 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 c7dfde86d..607dfadf1 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 000000000..9a75dd51e --- /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 0be5448ee..7e42430c1 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 516a357cd..c25014cf6 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 6a2d6ab77..fb90cc61a 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 000000000..c7d4209e3 --- /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 0335eaf3d..5f75b2f60 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 c5b527a48..601442bdd 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 178592a2e..6a105f7ee 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 000000000..45313e56d --- /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 aa1b98115..3fa9b23a7 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 f9108fe85..8f7f1dc6b 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 10901dfdb..395d35939 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 000000000..7a6e3849d --- /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 000000000..f61f96cd1 --- /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 000000000..117ab4ed8 --- /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 000000000..f4a21dd44 --- /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 000000000..c9ac8c292 --- /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 000000000..d76bc065e --- /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 000000000..07225d682 --- /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 000000000..66f9820e4 --- /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 000000000..8cdcb8824 --- /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 000000000..c86965752 --- /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 000000000..7bf56c554 --- /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 000000000..5d48a2759 --- /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 edf112d12..dec88b2ab 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 bc02d3eaa..35d930700 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 770586526..f05a3bf36 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 a63d069b0..43370157b 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 d2f2fd7c5..9ea015a53 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 a02df8653..ebec882a1 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 b81f5299e..46916174a 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"] }