diff --git a/cypress/component/DataSubmission/v2/ds_nih_administrative_information.spec.tsx b/cypress/component/DataSubmission/v2/ds_nih_administrative_information.spec.tsx new file mode 100644 index 0000000000..2820078531 --- /dev/null +++ b/cypress/component/DataSubmission/v2/ds_nih_administrative_information.spec.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { mount } from 'cypress/react' +import { cloneDeep } from 'lodash' +import { Study, NihAnvilUse, StudyProperty } from 'src/pages/data_submission/v2/v2-models' +import { + NihAdministrativeInformation, + NihAdministrativeInformationProps, +} from 'src/pages/data_submission/v2/NihAdministrativeInformation' +import { InstitutionInterface } from 'src/types/model' + +let propCopy = { study: { properties: [] as StudyProperty[] } as Study } as NihAdministrativeInformationProps + +const props = { + setStudy: () => {}, + study: { properties: [] }, + setFiles: () => {}, +} + +const mockInstitutions = [ + { + id: 1, + name: 'Test Institution 1', + domains: ['test1.edu'], + signingOfficials: [{ userId: '1', displayName: 'User 1', email: 'email1' }], + createDate: 'Feb 1, 2023', + createUser: 1, + createUserId: 1, + } as unknown as InstitutionInterface, + { + id: 2, + name: 'Test Institution 2', + domains: ['test2.edu'], + signingOfficials: [{ userId: '2', displayName: 'User 2', email: 'email2' }], + createDate: 'Jul 1, 2025', + createUser: 1, + createUserId: 1, + updateDate: 'Jul 2, 2025', + updateUser: 1, + updateUserId: 1, + } as unknown as InstitutionInterface, +] + +beforeEach(() => { + propCopy = cloneDeep(props) + cy.initApplicationConfig() + cy.intercept('GET', '/api/institutions', (req) => { + req.reply({ + delay: 1000, // Simulate a delay to show loading state + body: mockInstitutions, + }) + }) +}) + +describe('NihAdministrativeInformation - Tests', () => { + it('should mount without any fields in the NihAdministrativeInformation', () => { + mount() + cy.get('.formField-container').should('not.exist') + }) + + it('fields should be visible if the user selected I am NHGRI funded and I have a dbGaP PHS ID already', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + mount() + cy.get('#piInstitution').should('be.visible') + cy.get('#nihGrantContractNumber').should('be.visible') + cy.get('#nihICsSupportingStudy').should('be.visible') + cy.get('#nihProgramOfficerName').should('be.visible') + cy.get('#nihInstitutionCenterSubmission').should('be.visible') + cy.get('#nihGenomicProgramAdministratorName').should('be.visible') + cy.get('#multiCenterStudy').should('be.visible') + cy.get('#controlledAccessRequiredForGenomicSummaryResultsGSR').should('be.visible') + }) + + it('fields should be visible if the user selected I am NHGRI funded and I do not have a dbGaP PHS ID', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_NO_PHS_ID)) + mount() + cy.get('#piInstitution').should('be.visible') + cy.get('#nihGrantContractNumber').should('be.visible') + cy.get('#nihICsSupportingStudy').should('be.visible') + cy.get('#nihProgramOfficerName').should('be.visible') + cy.get('#nihInstitutionCenterSubmission').should('be.visible') + cy.get('#nihGenomicProgramAdministratorName').should('be.visible') + cy.get('#multiCenterStudy').should('be.visible') + cy.get('#controlledAccessRequiredForGenomicSummaryResultsGSR').should('be.visible') + }) + + it('should hide dbGaP form fields if the user selected I am not NHGRI funded but I am seeking to submit data to AnVIL', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_YES_ANVIL)) + mount() + cy.get('#piInstitution').should('be.visible') + cy.get('#nihGrantContractNumber').should('be.visible') + cy.get('#nihICsSupportingStudy').should('be.visible') + cy.get('#nihProgramOfficerName').should('be.visible') + cy.get('#nihInstitutionCenterSubmission').should('be.visible') + cy.get('#nihGenomicProgramAdministratorName').should('be.visible') + cy.get('#multiCenterStudy').should('be.visible') + cy.get('#controlledAccessRequiredForGenomicSummaryResultsGSR').should('be.visible') + }) + + it('should hide dbGaP form fields if the user selected I am not NHGRI funded and do not plan to store data in AnVIL', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_NO_ANVIL)) + mount() + cy.get('.formField-container').should('not.exist') + cy.get('#piInstitution').should('not.exist') + cy.get('#nihGrantContractNumber').should('not.exist') + cy.get('#nihICsSupportingStudy').should('not.exist') + cy.get('#nihProgramOfficerName').should('not.exist') + cy.get('#nihInstitutionCenterSubmission').should('not.exist') + cy.get('#nihGenomicProgramAdministratorName').should('not.exist') + cy.get('#multiCenterStudy').should('not.exist') + cy.get('#controlledAccessRequiredForGenomicSummaryResultsGSR').should('not.exist') + }) +}) diff --git a/cypress/component/DataSubmission/v2/ds_nih_anvil_use.spec.tsx b/cypress/component/DataSubmission/v2/ds_nih_anvil_use.spec.tsx new file mode 100644 index 0000000000..d3249541c4 --- /dev/null +++ b/cypress/component/DataSubmission/v2/ds_nih_anvil_use.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { mount } from 'cypress/react' +import { cloneDeep } from 'lodash' +import { NihAnvilUseRelated, NihAnvilUseRelatedProps } from 'src/pages/data_submission/v2/NihAnvilUseRelated' +import { Study, NihAnvilUse, StudyProperty } from 'src/pages/data_submission/v2/v2-models' + +let propCopy = { study: { properties: [] as StudyProperty[] } as Study } as NihAnvilUseRelatedProps + +const props = { + setStudy: () => {}, + study: { properties: [] }, + setFiles: () => {}, +} + +beforeEach(() => { + propCopy = cloneDeep(props) +}) + +describe('NihAnvilUseRelated - Tests', () => { + it('should mount with only the nihAnvilUse form field displayed', () => { + mount() + cy.get('.formField-container').should('exist') + + cy.get('#nihAnvilUse').should('exist') + cy.get('#dbGaPPhsID').should('not.exist') + cy.get('#dbGaPStudyRegistrationName').should('not.exist') + cy.get('#embargoReleaseDate').should('not.exist') + cy.get('#sequencingCenter').should('not.exist') + }) + + it('should show dbGaP form fields if NHGRI funded and has dbGaP ID', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + mount() + cy.get(':nth-child(1) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + cy.get('#dbGaPPhsID').should('exist') + cy.get('#dbGaPStudyRegistrationName').should('exist') + cy.get('#embargoReleaseDate').should('exist') + cy.get('#sequencingCenter').should('exist') + }) + + it('should hide dbGaP form fields if NHGRI funded and no dbGaP ID', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_NO_PHS_ID)) + mount() + cy.get(':nth-child(2) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + + cy.get('#dbGaPPhsID').should('not.exist') + cy.get('#dbGaPStudyRegistrationName').should('not.exist') + cy.get('#embargoReleaseDate').should('not.exist') + cy.get('#sequencingCenter').should('not.exist') + }) + + it('should hide dbGaP form fields if not NHGRI funded and submitting to AnVIL ', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_YES_ANVIL)) + mount() + cy.get(':nth-child(3) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + + cy.get('#dbGaPPhsID').should('not.exist') + cy.get('#dbGaPStudyRegistrationName').should('not.exist') + cy.get('#embargoReleaseDate').should('not.exist') + cy.get('#sequencingCenter').should('not.exist') + }) + + it('should hide dbGaP form fields if not NHGRI funded and not submitting to AnVIL', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_NO_ANVIL)) + mount() + cy.get(':nth-child(4) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + + cy.get('#dbGaPPhsID').should('not.exist') + cy.get('#dbGaPStudyRegistrationName').should('not.exist') + cy.get('#embargoReleaseDate').should('not.exist') + cy.get('#sequencingCenter').should('not.exist') + }) +}) diff --git a/cypress/component/DataSubmission/v2/ds_nih_data_management.spec.tsx b/cypress/component/DataSubmission/v2/ds_nih_data_management.spec.tsx new file mode 100644 index 0000000000..1c9249ecdd --- /dev/null +++ b/cypress/component/DataSubmission/v2/ds_nih_data_management.spec.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { mount } from 'cypress/react' +import { cloneDeep } from 'lodash' +import { + Study, + NihAnvilUse, + StudyProperty, + AlternativeDataSharingPlan, + AlternativeDataSharingPlanReasons, +} from 'src/pages/data_submission/v2/v2-models' +import { NihDataManagement, NihDataManagementProps } from 'src/pages/data_submission/v2/NihDataManagement' +import { FileProperty } from 'src/pages/data_submission/v2/DataSubmissionFormV2' + +let propCopy = { study: { properties: [] as StudyProperty[] } as Study } as NihDataManagementProps + +const props = { + setStudy: () => {}, + study: { properties: [] }, + files: {} as FileProperty, + setFiles: () => {}, +} + +beforeEach(() => { + propCopy = cloneDeep(props) +}) + +function verifySharingPlanTopLevelDisabled() { + cy.get('#legalRestrictions').should('not.exist') + cy.get('#isInformedConsentProcessesInadequate').should('not.exist') + cy.get('#alternativeDataSharingPlanExplanation').should('not.exist') + cy.get('#alternativeDataSharingPlanFile_fileName').should('not.exist') + cy.get('#alternativeDataSharingPlanDataSubmitted').should('not.exist') + cy.get('#alternativeDataSharingPlanDataReleased').should('not.exist') +} + +describe('NihAdministrativeInformation - Tests', () => { + it('should mount without any fields in the NihAdministrativeInformation', () => { + mount() + cy.get('.formField-container').should('not.exist') + cy.get('#alternativeDataSharingPlan').should('not.exist') + }) + + it('fields should be visible if the user selected I am NHGRI funded and I have a dbGaP PHS ID already', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + mount() + cy.get('#alternativeDataSharingPlan').should('be.visible') + verifySharingPlanTopLevelDisabled() + cy.get('#alternativeDataSharingPlan > :nth-child(1) > [style="font-family: Montserrat; font-size: 14px;"] > label').click() + }) + it('fields should appear as user selects Yes to an Alternative Data Sharing Plan', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + propCopy.study.properties?.push(new AlternativeDataSharingPlan(true)) + mount() + cy.get('#legalRestrictions').should('be.visible') + cy.get('#isInformedConsentProcessesInadequate').should('be.visible') + cy.get('#alternativeDataSharingPlanExplanation').should('be.visible') + cy.get('#alternativeDataSharingPlanFile_fileName').should('be.visible') + cy.get('#alternativeDataSharingPlanDataSubmitted').should('be.visible') + cy.get('#alternativeDataSharingPlanDataReleased').should('be.visible') + }) + it('fields should appear if the user selects yes to inadequate consent process', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + propCopy.study.properties?.push(new AlternativeDataSharingPlan(true)) + propCopy.study.properties?.push(new AlternativeDataSharingPlanReasons([AlternativeDataSharingPlanReasons.VALUES.isInformedConsentProcessesInadequate])) + mount() + cy.get('#consentFormsUnavailable').should('be.visible') + cy.get('#consentProcessDidNotAddressFutureUseOrBroadSharing').should('be.visible') + cy.get('#consentProcessPrecludesFutureUseOrBroadSharing').should('be.visible') + cy.get('#otherInformedConsentLimitationsOrConcerns').should('be.visible') + cy.get('#otherReasonForRequest').should('be.visible') + }) + + it('fields should appear as user selects No to an Alternative Data Sharing Plan', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_YES_PHS_ID)) + propCopy.study.properties?.push(new AlternativeDataSharingPlan(false)) + mount() + verifySharingPlanTopLevelDisabled() + }) + + it('fields should be visible if the user selected I am NHGRI funded and I do not have a dbGaP PHS ID', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.YES_NHGRI_NO_PHS_ID)) + mount() + cy.get('#alternativeDataSharingPlan').should('be.visible') + }) + + it('should hide dbGaP form fields if the user selected I am not NHGRI funded but I am seeking to submit data to AnVIL', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_YES_ANVIL)) + mount() + cy.get('#alternativeDataSharingPlan').should('be.visible') + }) + + it('should hide dbGaP form fields if the user selected I am not NHGRI funded and do not plan to store data in AnVIL', () => { + propCopy?.study?.properties?.push(new NihAnvilUse(NihAnvilUse.NO_NHGRI_NO_ANVIL)) + mount() + cy.get('#alternativeDataSharingPlan').should('not.exist') + }) +}) diff --git a/cypress/component/DataSubmission/v2/ds_study_information.spec.tsx b/cypress/component/DataSubmission/v2/ds_study_information.spec.tsx new file mode 100644 index 0000000000..fd20f43355 --- /dev/null +++ b/cypress/component/DataSubmission/v2/ds_study_information.spec.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { mount } from 'cypress/react' +import { cloneDeep } from 'lodash' +import { + GeneralStudyInformation, GeneralStudyInformationProps, +} from 'src/pages/data_submission/v2/GeneralStudyInformation' + +let propCopy = {} as GeneralStudyInformationProps + +const props = { + setStudy: () => {}, + study: {}, +} + +beforeEach(() => { + propCopy = cloneDeep(props) +}) + +describe('GeneralStudyInformation - Tests', () => { + it('should mount with all the fields', () => { + mount() + cy.get('.formField-container').should('have.length', 11) + + cy.get('.formField-studyName').should('have.length', 1) + cy.get('.formField-studyType').should('have.length', 1) + cy.get('.formField-studyDescription').should('have.length', 1) + cy.get('.formField-dataTypes').should('have.length', 1) + cy.get('.formField-phenotypeIndication').should('have.length', 1) + cy.get('.formField-species').should('have.length', 1) + cy.get('.formField-piName').should('have.length', 1) + cy.get('.formField-dataCustodianEmail').should('have.length', 1) + cy.get('#alternativeDataSharingPlanTargetDeliveryDate').should('exist') + cy.get('#alternativeDataSharingPlanTargetPublicReleaseDate').should('exist') + cy.get('.formField-publicVisibility').should('have.length', 1) + }) + + it('should allow edit in all fields', () => { + cy.spy(propCopy, 'setStudy').as('setStudySpy') + mount() + cy.get('.formField-studyName').type('A Study Name') + cy.get('@setStudySpy').its('callCount').should('eq', 12) + cy.get('.formField-studyType').type('Observational{enter}') + cy.get('@setStudySpy').its('callCount').should('eq', 13) + cy.get('.formField-studyDescription').type('My description') + cy.get('@setStudySpy').its('callCount').should('eq', 27) + cy.get('.formField-dataTypes').type('dt{enter}') + cy.get('@setStudySpy').its('callCount').should('eq', 28) + cy.get('.formField-phenotypeIndication').type('pi') + cy.get('@setStudySpy').its('callCount').should('eq', 30) + cy.get('.formField-species').type('species') + cy.get('@setStudySpy').its('callCount').should('eq', 37) + cy.get('.formField-piName').type('name') + cy.get('@setStudySpy').its('callCount').should('eq', 41) + cy.get('#dataCustodianEmail > .css-13cymwt-control > .css-hlgwow > .css-19bb58m').type('abc@def.ghi{enter}') + cy.get('@setStudySpy').its('callCount').should('eq', 42) + cy.get('#alternativeDataSharingPlanTargetDeliveryDate').type('2025-04-01') + cy.get('@setStudySpy').its('callCount').should('eq', 43) + cy.get('#alternativeDataSharingPlanTargetPublicReleaseDate').type('2025-04-01') + cy.get('@setStudySpy').its('callCount').should('eq', 44) + cy.get(':nth-child(2) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + cy.get('@setStudySpy').its('callCount').should('eq', 45) + cy.get(':nth-child(1) > [style="font-family: Montserrat; font-size: 14px;"] > label > [style="float: left;"] > span').click() + cy.get('@setStudySpy').its('callCount').should('eq', 46) + }) +}) diff --git a/cypress/component/Institutions/AdminManageInstitutions.spec.tsx b/cypress/component/Institutions/AdminManageInstitutions.spec.tsx index 5dfae08029..702c2c45ab 100644 --- a/cypress/component/Institutions/AdminManageInstitutions.spec.tsx +++ b/cypress/component/Institutions/AdminManageInstitutions.spec.tsx @@ -3,7 +3,7 @@ import { mount } from 'cypress/react' import AdminManageInstitutions from 'src/pages/AdminManageInstitutions' import { BrowserRouter } from 'react-router-dom' import { Institution as InstitutionAPI } from 'src/libs/ajax/Institution' -import { DuosUser, Institution } from 'src/types/model' +import { DuosUser, InstitutionInterface } from 'src/types/model' const createUser: DuosUser = { createDate: new Date(), @@ -58,7 +58,7 @@ const mockInstitutions = [ createDate: 'Feb 1, 2023', createUser: createUser, createUserId: createUser.userId, - } as unknown as Institution, + } as unknown as InstitutionInterface, { id: 2, name: 'Test Institution 2', @@ -70,7 +70,7 @@ const mockInstitutions = [ updateDate: 'Jul 2, 2025', updateUser: updateUser, updateUserId: updateUser.userId, - } as unknown as Institution, + } as unknown as InstitutionInterface, ] describe('AdminManageInstitutions', () => { diff --git a/cypress/component/Institutions/InstitutionDomainEditor.spec.tsx b/cypress/component/Institutions/InstitutionDomainEditor.spec.tsx index 30d55e3647..80ca452000 100644 --- a/cypress/component/Institutions/InstitutionDomainEditor.spec.tsx +++ b/cypress/component/Institutions/InstitutionDomainEditor.spec.tsx @@ -2,7 +2,7 @@ import React from 'react' import { mount } from 'cypress/react' import { InstitutionDomainEditor } from 'src/components/institution_table/components/InstitutionDomainEditor' -import { Institution } from 'src/types/model' +import { InstitutionInterface } from 'src/types/model' describe('Institution Domain Editor Tests', () => { const testDomains = ['example.com', 'test.edu', 'domain.org'] @@ -171,7 +171,7 @@ describe('Institution Domain Editor Tests', () => { { id: 1, name: 'Institution A', domains: ['a.com', 'b.com'] }, { id: 2, name: 'Institution B', domains: ['c.com'] }, { id: 3, name: 'Institution C' }, - ] as Institution[] + ] as InstitutionInterface[] const onDomainsChange = cy.stub().as('domainsChangeHandler') mount( diff --git a/cypress/component/Institutions/InstitutionTable.spec.tsx b/cypress/component/Institutions/InstitutionTable.spec.tsx index 3954693af4..5a5d4b36e6 100644 --- a/cypress/component/Institutions/InstitutionTable.spec.tsx +++ b/cypress/component/Institutions/InstitutionTable.spec.tsx @@ -1,7 +1,7 @@ import React from 'react' import { mount } from 'cypress/react' import InstitutionTable, { InstitutionTableProps } from 'src/components/institution_table/InstitutionTable' -import { DuosUser, Institution } from 'src/types/model' +import { DuosUser, InstitutionInterface } from 'src/types/model' import { BrowserRouter } from 'react-router-dom' const createUser: DuosUser = { @@ -57,7 +57,7 @@ export const mockInstitutions = [ createDate: 'Feb 1, 2023', createUser: createUser, createUserId: createUser.userId, - } as unknown as Institution, + } as unknown as InstitutionInterface, { id: 2, name: 'Test Institution 2', @@ -69,7 +69,7 @@ export const mockInstitutions = [ updateDate: 'Jul 2, 2025', updateUser: updateUser, updateUserId: updateUser.userId, - } as unknown as Institution, + } as unknown as InstitutionInterface, ] const defaultProps = { diff --git a/src/components/forms/DataTypes.ts b/src/components/forms/DataTypes.ts index 196dfce3ad..adebe9cd24 100644 --- a/src/components/forms/DataTypes.ts +++ b/src/components/forms/DataTypes.ts @@ -1,49 +1,49 @@ import { SelectOptionWithKeyNameAndAbbreviation } from './SelectOptionInterface' export class DataTypes { - static CITE: SelectOptionWithKeyNameAndAbbreviation = { + static readonly CITE: SelectOptionWithKeyNameAndAbbreviation = { key: 'CITE', name: 'CITE-seq', } - static HYB: SelectOptionWithKeyNameAndAbbreviation = { + static readonly HYB: SelectOptionWithKeyNameAndAbbreviation = { key: 'HYB', name: 'Hybrid Capture', } - static RNA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly RNA: SelectOptionWithKeyNameAndAbbreviation = { key: 'RNA', name: 'RNA-Seq', } - static SCRNA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly SCRNA: SelectOptionWithKeyNameAndAbbreviation = { key: 'scRNA', name: 'scRNA-Seq', } - static SPT: SelectOptionWithKeyNameAndAbbreviation = { + static readonly SPT: SelectOptionWithKeyNameAndAbbreviation = { key: 'SPT', name: 'Spatial Transcriptomics', } - static SNRNA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly SNRNA: SelectOptionWithKeyNameAndAbbreviation = { key: 'snRNA', name: 'snRNA-Seq', } - static WGS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly WGS: SelectOptionWithKeyNameAndAbbreviation = { key: 'WGS', abbreviation: 'WGS', name: 'Whole Genome', } - static WES: SelectOptionWithKeyNameAndAbbreviation = { + static readonly WES: SelectOptionWithKeyNameAndAbbreviation = { key: 'WES', abbreviation: 'WES', name: 'Whole Exome', } - static VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [DataTypes.CITE, + static readonly VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [DataTypes.CITE, DataTypes.HYB, DataTypes.RNA, DataTypes.SCRNA, diff --git a/src/components/forms/InstitutionPicker.tsx b/src/components/forms/InstitutionPicker.tsx new file mode 100644 index 0000000000..e8d8ff779e --- /dev/null +++ b/src/components/forms/InstitutionPicker.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react' +import { MasterChangeHandler } from 'src/pages/data_submission/v2/v2-common-functions' +import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms' +import { isEmpty, isNil } from 'lodash/fp' +import { Institution } from 'src/libs/ajax/Institution' +import { InstitutionInterface } from 'src/types/model' +import { Notifications } from 'src/libs/utils' + +export interface InstitutionPickerProps { + initialInstitution: number + fieldTitle: string + fieldId: string + isRequired: boolean + onChange: MasterChangeHandler +} + +export const InstitutionPicker = (props: InstitutionPickerProps) => { + const { initialInstitution, fieldId, fieldTitle, isRequired, onChange } = props + const [institutionList, setInstitutionList] = useState([]) + + useEffect(() => { + const getAllInstitutions = async () => { + const institutions = await Institution.list() + setInstitutionList(institutions) + } + + const init = async () => { + try { + await getAllInstitutions() + } + catch (error) { + Notifications.showError({ + text: 'Error: Unable to initialize data from server' + error, + }) + } + } + init() + }, []) + + const findInstitutionSelectOption = (id: number) => { + const institution = institutionList.find(inst => inst.id === id) + + return { + displayText: institution?.name || 'Unknown', + id: id, + } + } + + const validation = isRequired ? [FormValidators.REQUIRED] : [] + + return ( + ({ displayText: inst.name, id: inst.id }))} + isCreatable={false} + selectConfig={{}} + onChange={({ key, value, isValid }: { key: string, value: { displayText: string, id: number }, isValid: boolean }) => { + onChange({ key, value: value?.id, isValid }) + }} + defaultValue={isNil(initialInstitution) ? null : findInstitutionSelectOption(initialInstitution)} + /> + ) +} diff --git a/src/components/forms/NIHInstitutesAndCenters.ts b/src/components/forms/NIHInstitutesAndCenters.ts index 3f524e49a8..bdb3de5dda 100644 --- a/src/components/forms/NIHInstitutesAndCenters.ts +++ b/src/components/forms/NIHInstitutesAndCenters.ts @@ -1,164 +1,164 @@ import { SelectOptionWithKeyNameAndAbbreviation } from './SelectOptionInterface' export class NIHInstitutesAndCenters { - static NCI: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NCI: SelectOptionWithKeyNameAndAbbreviation = { key: 'NCI', name: 'National Cancer Institute', abbreviation: 'NCI', } - static NEI: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NEI: SelectOptionWithKeyNameAndAbbreviation = { key: 'NEI', name: 'National Eye Institute', abbreviation: 'NEI', } - static NHLBI: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NHLBI: SelectOptionWithKeyNameAndAbbreviation = { key: 'NHLBI', name: 'National Heart, Lung, and Blood Institute', abbreviation: 'NHLBI', } - static NHGRI: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NHGRI: SelectOptionWithKeyNameAndAbbreviation = { key: 'NHGRI', name: 'National Human Genome Research Institute', abbreviation: 'NHGRI', } - static NIA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIA: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIA', name: 'National Institute on Aging', abbreviation: 'NIA', } - static NIAAA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIAAA: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIAAA', name: 'National Institute on Alcohol Abuse and Alcoholism', abbreviation: 'NIAAA', } - static NIAID: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIAID: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIAID', name: 'National Institute of Allergy and Infectious Diseases', abbreviation: 'NIAID', } - static NIAMS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIAMS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIAMS', name: 'National Institute of Arthritis and Musculoskeletal and Skin Diseases', abbreviation: 'NIAMS', } - static NIBIB: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIBIB: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIBIB', name: 'National Institute of Biomedical Imaging and Bioengineering', abbreviation: 'NIBIB', } - static NICHD: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NICHD: SelectOptionWithKeyNameAndAbbreviation = { key: 'NICHD', name: 'Eunice Kennedy Shriver National Institute of Child Health and Human Development', abbreviation: 'NICHD', } - static NIDCD: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIDCD: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIDCD', name: 'National Institute on Deafness and Other Communication Disorders', abbreviation: 'NIDCD', } - static NIDCR: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIDCR: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIDCR', name: 'National Institute of Dental and Craniofacial Research', abbreviation: 'NIDCR', } - static NIDDK: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIDDK: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIDDK', name: 'National Institute of Diabetes and Digestive and Kidney Diseases', abbreviation: 'NIDDK', } - static NIDA: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIDA: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIDA', name: 'National Institute on Drug Abuse', abbreviation: 'NIDA', } - static NIEHS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIEHS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIEHS', name: 'National Institute of Environmental Health Sciences', abbreviation: 'NIEHS', } - static NIGMS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIGMS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIGMS', name: 'National Institute of General Medical Sciences', abbreviation: 'NIGMS', } - static NIMH: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIMH: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIMH', name: 'National Institute of Mental Health', abbreviation: 'NIMH', } - static NIMHD: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NIMHD: SelectOptionWithKeyNameAndAbbreviation = { key: 'NIMHD', name: 'National Institute on Minority Health and Health Disparities', abbreviation: 'NIMHD', } - static NINDS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NINDS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NINDS', name: 'National Institute of Neurological Disorders and Stroke', abbreviation: 'NINDS', } - static NINR: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NINR: SelectOptionWithKeyNameAndAbbreviation = { key: 'NINR', name: 'National Institute of Nursing Research', abbreviation: 'NINR', } - static NLM: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NLM: SelectOptionWithKeyNameAndAbbreviation = { key: 'NLM', name: 'National Library of Medicine', abbreviation: 'NLM', } - static CC: SelectOptionWithKeyNameAndAbbreviation = { key: 'CC', name: 'NIH Clinical Center', abbreviation: 'CC' } - static CIT: SelectOptionWithKeyNameAndAbbreviation = { + static readonly CC: SelectOptionWithKeyNameAndAbbreviation = { key: 'CC', name: 'NIH Clinical Center', abbreviation: 'CC' } + static readonly CIT: SelectOptionWithKeyNameAndAbbreviation = { key: 'CIT', name: 'Center for Information Technology', abbreviation: 'CIT', } - static CSR: SelectOptionWithKeyNameAndAbbreviation = { + static readonly CSR: SelectOptionWithKeyNameAndAbbreviation = { key: 'CSR', name: 'Center for Scientific Review', abbreviation: 'CSR', } - static FIC: SelectOptionWithKeyNameAndAbbreviation = { + static readonly FIC: SelectOptionWithKeyNameAndAbbreviation = { key: 'FIC', name: 'Fogarty International Center', abbreviation: 'FIC', } - static NCATS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NCATS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NCATS', name: 'National Center for Advancing Translational Sciences', abbreviation: 'NCATS', } - static NCCIH: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NCCIH: SelectOptionWithKeyNameAndAbbreviation = { key: 'NCCIH', name: 'National Center for Complementary and Integrative Health', abbreviation: 'NCCIH', } - static VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ + static readonly VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ NIHInstitutesAndCenters.NCI, NIHInstitutesAndCenters.NEI, NIHInstitutesAndCenters.NHLBI, diff --git a/src/components/forms/ResearchStage.ts b/src/components/forms/ResearchStage.ts index ec9af41045..127835d3a1 100644 --- a/src/components/forms/ResearchStage.ts +++ b/src/components/forms/ResearchStage.ts @@ -1,10 +1,10 @@ import { SelectOptionWithKeyNameAndAbbreviation } from './SelectOptionInterface' export class ResearchStage { - static PRA: SelectOptionWithKeyNameAndAbbreviation = { key: 'PRA', name: 'Pre-analysis' } - static POA: SelectOptionWithKeyNameAndAbbreviation = { key: 'POA', name: 'Post-analysis' } - static INA: SelectOptionWithKeyNameAndAbbreviation = { key: 'INA', name: 'Intra-analysis' } - static VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ + static readonly PRA: SelectOptionWithKeyNameAndAbbreviation = { key: 'PRA', name: 'Pre-analysis' } + static readonly POA: SelectOptionWithKeyNameAndAbbreviation = { key: 'POA', name: 'Post-analysis' } + static readonly INA: SelectOptionWithKeyNameAndAbbreviation = { key: 'INA', name: 'Intra-analysis' } + static readonly VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ ResearchStage.PRA, ResearchStage.POA, ResearchStage.INA, diff --git a/src/components/forms/SecondaryDataUseTerms.ts b/src/components/forms/SecondaryDataUseTerms.ts index 684aa05f6e..3fcb878df0 100644 --- a/src/components/forms/SecondaryDataUseTerms.ts +++ b/src/components/forms/SecondaryDataUseTerms.ts @@ -1,52 +1,52 @@ import { SelectOptionWithKeyNameAndAbbreviation } from './SelectOptionInterface' export class SecondaryDataUseTerms { - static NMDS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly NMDS: SelectOptionWithKeyNameAndAbbreviation = { key: 'NMDS', name: 'No methods development or validation studies', abbreviation: 'NMDS', } - static GSO: SelectOptionWithKeyNameAndAbbreviation = { + static readonly GSO: SelectOptionWithKeyNameAndAbbreviation = { key: 'GSO', name: 'Genetic studies only', abbreviation: 'GSO', } - static PUB: SelectOptionWithKeyNameAndAbbreviation = { + static readonly PUB: SelectOptionWithKeyNameAndAbbreviation = { key: 'PUB', name: 'Publication Required', abbreviation: 'PUB', } - static COL: SelectOptionWithKeyNameAndAbbreviation = { + static readonly COL: SelectOptionWithKeyNameAndAbbreviation = { key: 'COL', name: 'Collaboration Required', abbreviation: 'COL', } - static IRB: SelectOptionWithKeyNameAndAbbreviation = { + static readonly IRB: SelectOptionWithKeyNameAndAbbreviation = { key: 'IRB', name: 'Ethics Approval Required', abbreviation: 'IRB', } - static GSMINUS: SelectOptionWithKeyNameAndAbbreviation = { + static readonly GSMINUS: SelectOptionWithKeyNameAndAbbreviation = { key: 'GS-', name: 'Geographic Restriction', abbreviation: 'GS-', } - static MOR: SelectOptionWithKeyNameAndAbbreviation = { + static readonly MOR: SelectOptionWithKeyNameAndAbbreviation = { key: 'MOR', name: 'Publication Moratorium', abbreviation: 'MOR', } - static NPU: SelectOptionWithKeyNameAndAbbreviation = { key: 'NPU', name: 'Non-profit Use Only', abbreviation: 'NPU' } - static OTH: SelectOptionWithKeyNameAndAbbreviation = { key: 'OTH', name: 'Other' } + static readonly NPU: SelectOptionWithKeyNameAndAbbreviation = { key: 'NPU', name: 'Non-profit Use Only', abbreviation: 'NPU' } + static readonly OTH: SelectOptionWithKeyNameAndAbbreviation = { key: 'OTH', name: 'Other' } - static VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ + static readonly VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ SecondaryDataUseTerms.NMDS, SecondaryDataUseTerms.GSO, SecondaryDataUseTerms.PUB, diff --git a/src/components/forms/SelectOptionsInterfacePicker.tsx b/src/components/forms/SelectOptionsInterfacePicker.tsx new file mode 100644 index 0000000000..6c0c35cb19 --- /dev/null +++ b/src/components/forms/SelectOptionsInterfacePicker.tsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react' +import { SelectOptionWithKeyNameAndAbbreviation } from 'src/components/forms/SelectOptionInterface' +import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms' + +export interface SelectInterfacePickerSelection { + displayText: string + key: string +} + +export interface SelectOptionsInterfacePickerProps { + fieldId: string + initialValue: string | string[] + onChange: ({ value, isValid }: { value: SelectInterfacePickerSelection | undefined | SelectInterfacePickerSelection[], isValid: boolean }) => void + optionList: SelectOptionWithKeyNameAndAbbreviation[] + fieldTitle: string + fieldPlaceholder: string + isRequired?: boolean + isMulti?: boolean +} + +export const SelectOptionsInterfacePicker = (props: SelectOptionsInterfacePickerProps) => { + const { fieldId, fieldTitle, fieldPlaceholder, initialValue, onChange, optionList, isRequired, isMulti } = props + + const getDisplayName = (value: SelectOptionWithKeyNameAndAbbreviation | undefined) => { + if (!value) { + return (`Error loading choice`) + } + return (`${value.name} (${value.abbreviation})`) + } + + const optionsListWithNameAndKey = () => { + return optionList.map((val) => { + return { displayText: getDisplayName(val), key: val.key } + }) + } + + const fullOptionsListWithNameAndKey = optionsListWithNameAndKey() + + const [formattedOptionsList, setFormattedOptionsList] = useState(optionsListWithNameAndKey()) + + const getInitialValueFromKey = (key: undefined | string | string[]) => { + if (Array.isArray(key)) { + return key.map(akey => fullOptionsListWithNameAndKey.find(value => value.key == akey)) + } + else if (!key) { + return null + } + return fullOptionsListWithNameAndKey.find(value => value.key == key) + } + + return ( + { + if (isValid) { + if (isMulti && value && Array.isArray(value)) { + const keys = new Set(value.map(entry => entry.key)) + setFormattedOptionsList(fullOptionsListWithNameAndKey.filter(entry => !keys.has(entry.key))) + } + onChange({ value, isValid }) + } + }} + type={FormFieldTypes.SELECT} + defaultValue={getInitialValueFromKey(initialValue)} + selectOptions={formattedOptionsList} + isMulti={isMulti} + /> + ) +} diff --git a/src/components/forms/StudyType.ts b/src/components/forms/StudyType.ts index 47f5944160..d66e46f1e4 100644 --- a/src/components/forms/StudyType.ts +++ b/src/components/forms/StudyType.ts @@ -1,17 +1,17 @@ import { SelectOptionWithKeyNameAndAbbreviation } from './SelectOptionInterface' export class StudyType { - static OBS: SelectOptionWithKeyNameAndAbbreviation = { key: 'OBS', name: 'Observational' } - static INT: SelectOptionWithKeyNameAndAbbreviation = { key: 'INT', name: 'Interventional' } - static DES: SelectOptionWithKeyNameAndAbbreviation = { key: 'DES', name: 'Descriptive' } - static ANA: SelectOptionWithKeyNameAndAbbreviation = { key: 'ANA', name: 'Analytical' } - static PRO: SelectOptionWithKeyNameAndAbbreviation = { key: 'PRO', name: 'Prospective' } - static RET: SelectOptionWithKeyNameAndAbbreviation = { key: 'RET', name: 'Retrospective' } - static CAR: SelectOptionWithKeyNameAndAbbreviation = { key: 'CAR', name: 'Case report' } - static CAS: SelectOptionWithKeyNameAndAbbreviation = { key: 'CAS', name: 'Case series' } - static CRS: SelectOptionWithKeyNameAndAbbreviation = { key: 'CRS', name: 'Cross-sectional' } - static COS: SelectOptionWithKeyNameAndAbbreviation = { key: 'COS', name: 'Cohort study' } - static VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ + static readonly OBS: SelectOptionWithKeyNameAndAbbreviation = { key: 'OBS', name: 'Observational' } + static readonly INT: SelectOptionWithKeyNameAndAbbreviation = { key: 'INT', name: 'Interventional' } + static readonly DES: SelectOptionWithKeyNameAndAbbreviation = { key: 'DES', name: 'Descriptive' } + static readonly ANA: SelectOptionWithKeyNameAndAbbreviation = { key: 'ANA', name: 'Analytical' } + static readonly PRO: SelectOptionWithKeyNameAndAbbreviation = { key: 'PRO', name: 'Prospective' } + static readonly RET: SelectOptionWithKeyNameAndAbbreviation = { key: 'RET', name: 'Retrospective' } + static readonly CAR: SelectOptionWithKeyNameAndAbbreviation = { key: 'CAR', name: 'Case report' } + static readonly CAS: SelectOptionWithKeyNameAndAbbreviation = { key: 'CAS', name: 'Case series' } + static readonly CRS: SelectOptionWithKeyNameAndAbbreviation = { key: 'CRS', name: 'Cross-sectional' } + static readonly COS: SelectOptionWithKeyNameAndAbbreviation = { key: 'COS', name: 'Cohort study' } + static readonly VALUES: SelectOptionWithKeyNameAndAbbreviation[] = [ StudyType.OBS, StudyType.INT, StudyType.DES, @@ -23,4 +23,6 @@ export class StudyType { StudyType.CRS, StudyType.COS, ] + + static readonly NAME_VALUES: string[] = StudyType.VALUES.map(val => val.name) } diff --git a/src/components/institution_table/InstitutionDetails.tsx b/src/components/institution_table/InstitutionDetails.tsx index 04bcc23228..ab327ece73 100644 --- a/src/components/institution_table/InstitutionDetails.tsx +++ b/src/components/institution_table/InstitutionDetails.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Institution } from 'src/types/model' +import { InstitutionInterface } from 'src/types/model' import backArrowIcon from 'src/images/back_arrow.svg' import { Link, useNavigate, useParams } from 'react-router-dom' import { Institution as InstitutionAPI } from 'src/libs/ajax/Institution' @@ -26,11 +26,11 @@ export const InstitutionDetails = (props: InstitutionDetailsProps) => { const { institutionId } = params const formMode = props.formMode const navigate = useNavigate() - const [institutionList, setInstitutionList] = useState([]) + const [institutionList, setInstitutionList] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [isEditing, setIsEditing] = useState(formMode === FORM_MODES.createNew) - const [institution, setInstitution] = useState() + const [institution, setInstitution] = useState() const [institutionUpdates, setInstitutionUpdates] = useState({ name: '', domains: [], @@ -40,7 +40,7 @@ export const InstitutionDetails = (props: InstitutionDetailsProps) => { useEffect(() => { const loadInstitution = async () => { try { - const institutions: Institution[] = await InstitutionAPI.list() + const institutions: InstitutionInterface[] = await InstitutionAPI.list() setInstitutionList(institutions) if (institutionId && formMode === FORM_MODES.editExisting) { const institution = institutions.find(inst => inst.id.toString() === institutionId.toString()) diff --git a/src/components/institution_table/InstitutionTable.tsx b/src/components/institution_table/InstitutionTable.tsx index 2e6c1499cd..9f1a5ed0ac 100644 --- a/src/components/institution_table/InstitutionTable.tsx +++ b/src/components/institution_table/InstitutionTable.tsx @@ -15,11 +15,11 @@ import { storageInstitutionSort, tableStyles, } from 'src/components/institution_table/InstitutionTableUtils' -import { Institution } from 'src/types/model' +import { InstitutionInterface } from 'src/types/model' import { recalculateVisibleTable } from 'src/libs/utils' export interface InstitutionTableProps { - readonly filteredList: Institution[] + readonly filteredList: InstitutionInterface[] readonly currentPage: number readonly setCurrentPage: (page: number) => void readonly tableSize: number diff --git a/src/components/institution_table/InstitutionTableUtils.tsx b/src/components/institution_table/InstitutionTableUtils.tsx index eea9fb7cdf..b08da63fdf 100644 --- a/src/components/institution_table/InstitutionTableUtils.tsx +++ b/src/components/institution_table/InstitutionTableUtils.tsx @@ -1,6 +1,6 @@ import ReactTooltip from 'react-tooltip' import React from 'react' -import { Institution, SimplifiedDuosUser } from 'src/types/model' +import { InstitutionInterface, SimplifiedDuosUser } from 'src/types/model' import { Link } from 'react-router-dom' import { Storage } from 'src/libs/storage' import { isEmpty } from 'lodash' @@ -44,9 +44,9 @@ interface ColumnConfig { interface ColumnConfigCell { label: string cellStyle: React.CSSProperties - cellDataFn: (row: Institution) => React.ReactNode + cellDataFn: (row: InstitutionInterface) => React.ReactNode sortable?: boolean - sortValueFn?: (row: Institution) => React.ReactNode + sortValueFn?: (row: InstitutionInterface) => React.ReactNode } /** @@ -134,14 +134,14 @@ export const columnConfig: ColumnConfig = { id: { label: 'ID', cellStyle: { width: columnWidths.id }, - cellDataFn: (row: Institution) => row.id, + cellDataFn: (row: InstitutionInterface) => row.id, sortable: true, - sortValueFn: (row: Institution) => row.id, + sortValueFn: (row: InstitutionInterface) => row.id, }, name: { label: 'Institution', cellStyle: { width: columnWidths.name }, - cellDataFn: (row: Institution) => { + cellDataFn: (row: InstitutionInterface) => { if (row) { return ( row.name, + sortValueFn: (row: InstitutionInterface) => row.name, }, domains: { label: 'Domains', cellStyle: { width: columnWidths.domains }, - cellDataFn: (row: Institution) => { + cellDataFn: (row: InstitutionInterface) => { if (row?.domains) { return row.domains.toSorted((a: string, b: string) => { return a.localeCompare(b) @@ -177,7 +177,7 @@ export const columnConfig: ColumnConfig = { } }, sortable: true, - sortValueFn: (row: Institution) => { + sortValueFn: (row: InstitutionInterface) => { if (row?.domains) { return row.domains.toSorted((a: string, b: string) => { return a.localeCompare(b) @@ -191,7 +191,7 @@ export const columnConfig: ColumnConfig = { signingOfficials: { label: 'Signing Officials', cellStyle: { width: columnWidths.signingOfficials }, - cellDataFn: (row: Institution) => { + cellDataFn: (row: InstitutionInterface) => { if (row.signingOfficials && row.signingOfficials.length > 0) { const fullNames = row.signingOfficials.map((user: SimplifiedDuosUser) => `${user.displayName} (${user.email})`).join(', ') if (fullNames.length > 40) { @@ -237,12 +237,12 @@ export const columnConfig: ColumnConfig = { updateUser: { label: 'Updated By', cellStyle: { width: columnWidths.updateUser }, - cellDataFn: (row: Institution) => { + cellDataFn: (row: InstitutionInterface) => { const user = isEmpty(row.updateUser) ? row.createUser : row.updateUser return user?.displayName ?? '- -' }, sortable: true, - sortValueFn: (row: Institution) => { + sortValueFn: (row: InstitutionInterface) => { const user = isEmpty(row.updateUser) ? row.createUser : row.updateUser return user?.displayName ?? 'zz' // 'zz' ensures that institutions without a user appear at the end of the list }, @@ -250,7 +250,7 @@ export const columnConfig: ColumnConfig = { updateDate: { label: 'Updated On', cellStyle: { width: columnWidths.updateDate }, - cellDataFn: (row: Institution) => { + cellDataFn: (row: InstitutionInterface) => { if (row?.updateDate) { return row.updateDate } @@ -262,7 +262,7 @@ export const columnConfig: ColumnConfig = { } }, sortable: true, - sortValueFn: (row: Institution) => { + sortValueFn: (row: InstitutionInterface) => { // Institution dates are in the form of 'Mon D, YYYY', e.g. 'Jan 1, 2023' const dateString = row.updateDate || row.createDate if (dateString) { @@ -280,7 +280,7 @@ export const columnConfig: ColumnConfig = { * in the cell. * @param row Row of institution data to be processed. */ -export const processRowData = (row: Institution): CellData[] => { +export const processRowData = (row: InstitutionInterface): CellData[] => { const rowData: CellData[] = [] Object.keys(columnConfig).forEach((col) => { const { cellDataFn, cellStyle, label, sortValueFn } = columnConfig[col] @@ -299,7 +299,7 @@ export const processRowData = (row: Institution): CellData[] => { * Utility functions for the Institution Table. */ -export const calcPageCount = (tableSize: number, filteredList: Institution[]) => { +export const calcPageCount = (tableSize: number, filteredList: InstitutionInterface[]) => { if (isEmpty(filteredList)) { return 1 } @@ -312,7 +312,7 @@ export const columnHeaderData = (columns: string[]) => { return columns.map(col => columnConfig[col]) } -export const processRows = (institutions: Institution[]): CellData[][] => { +export const processRows = (institutions: InstitutionInterface[]): CellData[][] => { return institutions.map((institution) => { return processRowData(institution) }) diff --git a/src/components/institution_table/components/InstitutionDomainEditor.tsx b/src/components/institution_table/components/InstitutionDomainEditor.tsx index 560eee5b02..4c190cf540 100644 --- a/src/components/institution_table/components/InstitutionDomainEditor.tsx +++ b/src/components/institution_table/components/InstitutionDomainEditor.tsx @@ -1,20 +1,20 @@ import { Button, Chip, TextField } from '@mui/material' import React, { useState } from 'react' -import { Institution } from 'src/types/model' +import { InstitutionInterface } from 'src/types/model' interface DomainEditorProps { domains: string[] isEditing: boolean onDomainsChange?: (domains: string[]) => void - institutionList: Institution[] + institutionList: InstitutionInterface[] } export const InstitutionDomainEditor = ({ domains, isEditing, onDomainsChange, institutionList }: DomainEditorProps) => { const [tempDomain, setTempDomain] = useState('') const [errorMessage, setErrorMessage] = useState('') - const domainToInstitutionMap: Record = {} - institutionList.forEach((inst: Institution) => { + const domainToInstitutionMap: Record = {} + institutionList.forEach((inst: InstitutionInterface) => { (inst.domains || []).forEach((domain) => { domainToInstitutionMap[domain] = inst }) diff --git a/src/pages/AdminManageInstitutions.tsx b/src/pages/AdminManageInstitutions.tsx index a6281661ea..003692ca3b 100644 --- a/src/pages/AdminManageInstitutions.tsx +++ b/src/pages/AdminManageInstitutions.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { Institution as InstitutionAPI } from 'src/libs/ajax/Institution' -import { Institution } from 'src/types/model' +import { InstitutionInterface } from 'src/types/model' import { Styles } from '../libs/theme' import { getSearchFilterFunctions, Notifications } from 'src/libs/utils' import manageInstitutionsIcon from 'src/images/icon_manage_dac.png' @@ -12,8 +12,8 @@ import { Link } from 'react-router-dom' import { extractError } from 'src/utils/ErrorUtils' export default function AdminManageInstitutions() { - const [institutionList, setInstitutionList] = useState([]) - const [filteredList, setFilteredList] = useState([]) + const [institutionList, setInstitutionList] = useState([]) + const [filteredList, setFilteredList] = useState([]) const [tableSize, setTableSize] = useState(10) const [currentPage, setCurrentPage] = useState(1) const [isLoading, setIsLoading] = useState(false) @@ -48,7 +48,7 @@ export default function AdminManageInstitutions() { setFilteredList(filter(institutionList, query.current.value)) } - const filter = (list: Institution[], value: string): Institution[] => { + const filter = (list: InstitutionInterface[], value: string): InstitutionInterface[] => { if (value) { return getSearchFilterFunctions().institutions(value, list) } diff --git a/src/pages/data_submission/v2/DataSubmissionFormV2.tsx b/src/pages/data_submission/v2/DataSubmissionFormV2.tsx index 0f0474bb9d..deb0e7efde 100644 --- a/src/pages/data_submission/v2/DataSubmissionFormV2.tsx +++ b/src/pages/data_submission/v2/DataSubmissionFormV2.tsx @@ -1,43 +1,71 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router' import { DataSet } from 'src/libs/ajax/DataSet' -import { cloneDeep, set } from 'lodash/fp' import { GeneralStudyInformation } from 'src/pages/data_submission/v2/GeneralStudyInformation' import { NihAnvilUseRelated } from 'src/pages/data_submission/v2/NihAnvilUseRelated' import { Study } from 'src/pages/data_submission/v2/v2-models' -import { MasterChangeHandler } from 'src/pages/data_submission/v2/v2-common-functions' -import { useParams } from 'react-router-dom' +import { NihAdministrativeInformation } from 'src/pages/data_submission/v2/NihAdministrativeInformation' +import { NihDataManagement } from 'src/pages/data_submission/v2/NihDataManagement' +import { Styles } from 'src/libs/theme' +import lockIcon from 'src/images/lock-icon.png' + +export type FileProperty = { + key: string + value: File +} + +export const ALTERNATIVE_DATA_SHARING_PLAN_FILE = 'alternativeDataSharingPlanFile' export const DataSubmissionFormV2 = () => { const { studyId } = useParams() - const [formData, setFormData] = useState({} as Study) + const [study, setStudy] = useState({} as Study) + const [formFiles, setFormFiles] = useState({} as FileProperty) const [loadingError, setLoadingError] = useState(false) - const onChange: MasterChangeHandler = useCallback(({ key, value, isValid }: { key: string, value: unknown, isValid: boolean }) => { - if (isValid) { - setFormData((val: Study) => { - const newForm = cloneDeep(val) - return set(key, value, newForm) - }) - } - }, []) - useEffect(() => { const onLoadFormData = (studyId: string | undefined) => { if (studyId) { - DataSet.getStudyById(studyId).then(study => setFormData(study)).catch(() => { - setFormData({} as Study) + DataSet.getStudyById(studyId).then(study => setStudy(study)).catch(() => { + setStudy({} as Study) setLoadingError(true) }) } } onLoadFormData(studyId) - }, [studyId, setFormData]) + }, [studyId, setStudy]) return ( <> {loadingError &&
Error Loading Page
} - - +
+
+
+
+ Lock icon +
+
+
+ Study Registration Form +
+ Submit new datasets to DUOS +
+
+
+
+
+ + + + + +
) } diff --git a/src/pages/data_submission/v2/GeneralStudyInformation.tsx b/src/pages/data_submission/v2/GeneralStudyInformation.tsx index cd870189be..e2f5c5843e 100644 --- a/src/pages/data_submission/v2/GeneralStudyInformation.tsx +++ b/src/pages/data_submission/v2/GeneralStudyInformation.tsx @@ -2,7 +2,7 @@ import React from 'react' import { FormFieldTypes, FormField, FormValidators } from 'src/components/forms/forms' import { Study, - StudyType, + StudyTypeProperty, PhenotypeIndication, Species, DataCustodianEmail, @@ -10,50 +10,41 @@ import { AlternativeDataSharingPlanTargetPublicReleaseDate, } from 'src/pages/data_submission/v2/v2-models' import { - generateFormDateField, - generateFormTextField, - getStudyPropertyByKey, - MasterChangeHandler, + generateStudyInputFormTextField, + generateStudyPropertyFormDateField, + generateStudyPropertyFormTextField, + getStudyPropertyValueByKey, setStudyPropertyByKey, } from 'src/pages/data_submission/v2/v2-common-functions' +import { DataTypes } from 'src/components/forms/DataTypes' export interface GeneralStudyInformationProps { - formData: Study - onChange: MasterChangeHandler + study: Study + setStudy: React.Dispatch> } export const GeneralStudyInformation = (props: GeneralStudyInformationProps) => { const { - onChange, - formData, + setStudy, + study, } = props return (

Study Information

+ {generateStudyInputFormTextField(setStudy, 'studyName', study?.name, 'Study Name', 'Enter the study name', [FormValidators.REQUIRED])} - { - setStudyPropertyByKey(formData, onChange, input, new StudyType(input.value as string)) + setStudyPropertyByKey(study, setStudy, input, new StudyTypeProperty(input.value as string)) }} /> id="studyDescription" title="Study Description" placeholder="Description" - defaultValue={formData?.description} + defaultValue={study?.description} validators={[FormValidators.REQUIRED]} - onChange={onChange} + onChange={setStudy} /> isCreatable={true} isMulti={true} optionsAreString={true} - selectOptions={[ - // The top properties were extracted from the prod database and deduplicated using the query: - // SELECT property_value, COUNT(*) FROM dataset_property WHERE property_key = 2 GROUP BY property_value ORDER BY COUNT(*) DESC; - 'CITE-seq', - 'Hybrid Capture', - 'RNA-Seq', - 'scRNA-Seq', - 'Spatial Transcriptomics', - 'snRNA-Seq', - 'Whole Genome (WGS)', - 'Whole Exome (WES)', - ]} - defaultValue={formData?.dataTypes} - onChange={onChange} - /> - {generateFormTextField(formData, onChange, new PhenotypeIndication())} - {generateFormTextField(formData, onChange, new Species())} - (`${entry.name}` + (entry.abbreviation ? ` (${entry.abbreviation})` : '')))} + defaultValue={study?.dataTypes} + onChange={setStudy} /> + {generateStudyPropertyFormTextField(study, setStudy, new PhenotypeIndication())} + {generateStudyPropertyFormTextField(study, setStudy, new Species())} + {generateStudyInputFormTextField(setStudy, 'piName', study?.piName, 'Principal Investigator Name', 'Enter the Principal Investigator\'s name', [FormValidators.REQUIRED])} }, }} placeholder="Add one or more emails" - defaultValue={getStudyPropertyByKey(formData, 'dataCustodianEmail')} + defaultValue={getStudyPropertyValueByKey(study, 'dataCustodianEmail')} onChange={(input: { key: string[], value: unknown, isValid: boolean }) => { - setStudyPropertyByKey(formData, onChange, input, new DataCustodianEmail(input.value as string[])) + setStudyPropertyByKey(study, setStudy, input, new DataCustodianEmail(input.value as string[])) }} />
- {generateFormDateField(formData, onChange, new AlternativeDataSharingPlanTargetDeliveryDate(), [FormValidators.DATE], { width: '45%' })} - {generateFormDateField(formData, onChange, new AlternativeDataSharingPlanTargetPublicReleaseDate(), [FormValidators.DATE], { width: '45%' })} + {generateStudyPropertyFormDateField(study, setStudy, new AlternativeDataSharingPlanTargetDeliveryDate(), [FormValidators.DATE], { width: '45%' })} + {generateStudyPropertyFormDateField(study, setStudy, new AlternativeDataSharingPlanTargetPublicReleaseDate(), [FormValidators.DATE], { width: '45%' })}
{ name: true, text: 'Yes, I want my dataset info to be visible and available for requests' }, { name: false, text: 'No, I do not want my dataset info to be visible and available for requests' }, ]} - defaultValue={formData?.publicVisibility} - onChange={onChange} + defaultValue={study?.publicVisibility} + onChange={setStudy} />
) diff --git a/src/pages/data_submission/v2/NihAdministrativeInformation.tsx b/src/pages/data_submission/v2/NihAdministrativeInformation.tsx new file mode 100644 index 0000000000..efc9a4e5f4 --- /dev/null +++ b/src/pages/data_submission/v2/NihAdministrativeInformation.tsx @@ -0,0 +1,153 @@ +import React, { useEffect, useState } from 'react' +import { + CollaboratingSites, + ControlledAccessRequiredForGenomicSummaryResultsGSR, + ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation, + MultiCenterStudy, + NihAnvilUse, + NihGenomicProgramAdministratorName, + NihGrantContractNumber, + NihICsSupportingStudy, + NihInstitutionCenterSubmission, + NihProgramOfficerName, + PiInstitution, + Study, +} from 'src/pages/data_submission/v2/v2-models' +import { + generateStudyPropertyFormTextField, getStudyPropertyValueByKey, + removeStudyPropertiesByKeys, setStudyPropertyByKey, +} from 'src/pages/data_submission/v2/v2-common-functions' +import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms' +import { + SelectInterfacePickerSelection, + SelectOptionsInterfacePicker, +} from 'src/components/forms/SelectOptionsInterfacePicker' +import { InstitutionPicker } from 'src/components/forms/InstitutionPicker' +import { NIHInstitutesAndCenters } from 'src/components/forms/NIHInstitutesAndCenters' +import { cloneDeep } from 'lodash' + +export interface NihAdministrativeInformationProps { + study: Study + setStudy: React.Dispatch> +} + +export const NihAdministrativeInformation = (props: NihAdministrativeInformationProps) => { + const { setStudy, study } = props + const [isRequired, setIsRequired] = useState(false) + + useEffect(() => { + const nihAnvilUse = getStudyPropertyValueByKey(study, new NihAnvilUse().key) as string + setIsRequired(NihAnvilUse.requiresNIHAdministrativeInformation(nihAnvilUse)) + }, [study]) + + return ( + <>{isRequired && ( +
+

NIH Administrative Information

+ + {generateStudyPropertyFormTextField(study, setStudy, new NihGrantContractNumber(), [FormValidators.REQUIRED])} + entry.key) + setStudyPropertyByKey(study, setStudy, { isValid: isValid }, new NihICsSupportingStudy(myMap)) + } + else if (value) { + const myVal = value as SelectInterfacePickerSelection + const keys = [] as string[] + keys.push(myVal.key) + setStudyPropertyByKey(study, setStudy, { isValid: isValid }, new NihICsSupportingStudy(keys)) + } + else { + setStudyPropertyByKey(study, setStudy, { isValid: isValid }, new NihICsSupportingStudy([])) + } + }} + optionList={NIHInstitutesAndCenters.VALUES} + fieldTitle={NihICsSupportingStudy.fieldTitle} + fieldPlaceholder={NihICsSupportingStudy.fieldPlaceholderText} + isMulti={true} + /> + {generateStudyPropertyFormTextField(study, setStudy, new NihProgramOfficerName(), [FormValidators.REQUIRED])} + + {generateStudyPropertyFormTextField(study, setStudy, new NihGenomicProgramAdministratorName())} + { + setStudyPropertyByKey(study, setStudy, { isValid: true }, new MultiCenterStudy(value)) + if (!value) { + setStudy((val) => { + const newVal = cloneDeep(val) + removeStudyPropertiesByKeys(newVal, new Set([CollaboratingSites.key])) + return newVal + }) + } + }} + /> + {getStudyPropertyValueByKey(study, MultiCenterStudy.key) === true && ( + null, + }, + }} + placeholder="List site and hit enter here..." + defaultValue={getStudyPropertyValueByKey(study, CollaboratingSites.key)} + onChange={(_key: string, value: string[], isValid: boolean) => setStudyPropertyByKey(study, setStudy, { isValid: isValid }, new CollaboratingSites(value))} + /> + )} + + { + setStudyPropertyByKey(study, setStudy, { isValid: true }, new ControlledAccessRequiredForGenomicSummaryResultsGSR(value)) + if (!value) { + setStudy((val) => { + const newVal = cloneDeep(val) + removeStudyPropertiesByKeys(newVal, new Set([ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation.key])) + return newVal + }) + } + }} + /> + {getStudyPropertyValueByKey(study, ControlledAccessRequiredForGenomicSummaryResultsGSR.key) === true && generateStudyPropertyFormTextField(study, setStudy, new ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation())} +
+ )} + + ) +} diff --git a/src/pages/data_submission/v2/NihAnvilUseRelated.tsx b/src/pages/data_submission/v2/NihAnvilUseRelated.tsx index c0bf89fbe7..cdd889a16d 100644 --- a/src/pages/data_submission/v2/NihAnvilUseRelated.tsx +++ b/src/pages/data_submission/v2/NihAnvilUseRelated.tsx @@ -2,26 +2,34 @@ import React from 'react' import { Study, NihAnvilUse, DbGaPPhsID, - DbGaPStudyRegistrationName, EmbargoReleaseDate, SequencingCenter, + DbGaPStudyRegistrationName, EmbargoReleaseDate, SequencingCenter, PiInstitution, NihGrantContractNumber, + NihICsSupportingStudy, NihProgramOfficerName, NihInstitutionCenterSubmission, NihGenomicProgramAdministratorName, + MultiCenterStudy, CollaboratingSites, ControlledAccessRequiredForGenomicSummaryResultsGSR, + ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation, AlternativeDataSharingPlan, + AlternativeDataSharingPlanReasons, AlternativeDataSharingPlanExplanation, AlternativeDataSharingPlanDataSubmitted, + AlternativeDataSharingPlanDataReleased, } from 'src/pages/data_submission/v2/v2-models' import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms' import { - generateFormDateField, - generateFormTextField, - getStudyPropertyByKey, - MasterChangeHandler, + generateStudyPropertyFormDateField, + generateStudyPropertyFormTextField, + getStudyPropertyValueByKey, removeStudyPropertiesByKeys, setStudyPropertyByKey, } from 'src/pages/data_submission/v2/v2-common-functions' +import { cloneDeep, unset } from 'lodash' +import { ALTERNATIVE_DATA_SHARING_PLAN_FILE, FileProperty } from 'src/pages/data_submission/v2/DataSubmissionFormV2' export interface NihAnvilUseRelatedProps { - formData: Study - onChange: MasterChangeHandler + study: Study + setStudy: React.Dispatch> + setFiles: React.Dispatch> } export const NihAnvilUseRelated = (props: NihAnvilUseRelatedProps) => { const { - onChange, - formData, + setStudy, + study, + setFiles, } = props return ( @@ -32,18 +40,51 @@ export const NihAnvilUseRelated = (props: NihAnvilUseRelatedProps) => { title="Will you or did you submit data to the NIH?" type={FormFieldTypes.RADIOGROUP} options={NihAnvilUse.NIH_ANVIL_USE_RADIOGROUP_OPTIONS} - defaultValue={getStudyPropertyByKey(formData, 'nihAnvilUse')} + defaultValue={getStudyPropertyValueByKey(study, 'nihAnvilUse')} validators={[FormValidators.REQUIRED]} - onChange={(input: { key: string, value: unknown, isValid: boolean }) => { - setStudyPropertyByKey(formData, onChange, input, new NihAnvilUse(input.value as string)) + onChange={(input: { key: string, value: string | undefined, isValid: boolean }) => { + setStudyPropertyByKey(study, setStudy, input, new NihAnvilUse(input.value as string)) + if (NihAnvilUse.requiresNIHAdministrativeInformation(input.value)) { + setStudy((val) => { + const newVal = cloneDeep(val) + removeStudyPropertiesByKeys(newVal, new Set([DbGaPPhsID.key, + DbGaPStudyRegistrationName.key, + EmbargoReleaseDate.key, + SequencingCenter.key, + PiInstitution.key, + NihGrantContractNumber.key, + NihICsSupportingStudy.key, + NihProgramOfficerName.key, + NihInstitutionCenterSubmission.key, + NihGenomicProgramAdministratorName.key, + MultiCenterStudy.key, + CollaboratingSites.key, + ControlledAccessRequiredForGenomicSummaryResultsGSR.key, + ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation.key, + AlternativeDataSharingPlan.key, + AlternativeDataSharingPlanReasons.key, + AlternativeDataSharingPlanExplanation.key, + AlternativeDataSharingPlanDataSubmitted.key, + AlternativeDataSharingPlanDataReleased.key, + ])) + return newVal + }) + setFiles((val) => { + const newVal = cloneDeep(val) + if (val?.key === ALTERNATIVE_DATA_SHARING_PLAN_FILE) { + unset(newVal, ALTERNATIVE_DATA_SHARING_PLAN_FILE) + } + return newVal + }) + } }} /> - {getStudyPropertyByKey(formData, 'nihAnvilUse') === NihAnvilUse.YES_NHGRI_YES_PHS_ID && ( + {getStudyPropertyValueByKey(study, 'nihAnvilUse') === NihAnvilUse.YES_NHGRI_YES_PHS_ID && ( <> - {generateFormTextField(formData, onChange, new DbGaPPhsID(), [FormValidators.REQUIRED])} - {generateFormTextField(formData, onChange, new DbGaPStudyRegistrationName())} - {generateFormDateField(formData, onChange, new EmbargoReleaseDate(), [FormValidators.DATE])} - {generateFormTextField(formData, onChange, new SequencingCenter())} + {generateStudyPropertyFormTextField(study, setStudy, new DbGaPPhsID(), [FormValidators.REQUIRED])} + {generateStudyPropertyFormTextField(study, setStudy, new DbGaPStudyRegistrationName())} + {generateStudyPropertyFormDateField(study, setStudy, new EmbargoReleaseDate(), [FormValidators.DATE])} + {generateStudyPropertyFormTextField(study, setStudy, new SequencingCenter())} )} diff --git a/src/pages/data_submission/v2/NihDataManagement.tsx b/src/pages/data_submission/v2/NihDataManagement.tsx new file mode 100644 index 0000000000..17aa008bb8 --- /dev/null +++ b/src/pages/data_submission/v2/NihDataManagement.tsx @@ -0,0 +1,185 @@ +import React from 'react' +import { + AlternativeDataSharingPlan, + AlternativeDataSharingPlanDataReleased, AlternativeDataSharingPlanDataSubmitted, + AlternativeDataSharingPlanExplanation, AlternativeDataSharingPlanReasons, + NihAnvilUse, + Study, +} from 'src/pages/data_submission/v2/v2-models' +import { + generateStudyPropertyFormTextField, + generateStudyPropertyYesNoField, + getStudyPropertyValueByKey, + removeStudyPropertiesByKeys, setStudyPropertyByKey, +} from 'src/pages/data_submission/v2/v2-common-functions' +import { FormField, FormFieldTypes, FormValidators } from 'src/components/forms/forms' +import { cloneDeep, set, unset } from 'lodash' +import { ALTERNATIVE_DATA_SHARING_PLAN_FILE, FileProperty } from 'src/pages/data_submission/v2/DataSubmissionFormV2' + +export interface NihDataManagementProps { + study: Study + setStudy: React.Dispatch> + files: FileProperty + setFiles: React.Dispatch> +} +export const NihDataManagement = (props: NihDataManagementProps) => { + const { setStudy, setFiles, files, study } = props + + const onAlternativeDataSharingPlanReasonsChange = ({ key }: { key: string }) => { + let setReasons: string[] = getStudyPropertyValueByKey(study, AlternativeDataSharingPlanReasons.key) as string[] ?? [] + if (Object.keys(AlternativeDataSharingPlanReasons.VALUES).includes(key)) { + const target = AlternativeDataSharingPlanReasons.VALUES[key as keyof typeof AlternativeDataSharingPlanReasons.VALUES] + const index = setReasons.indexOf(target) + let removed = false + if (index > -1) { + setReasons.splice(index, 1) + removed = true + } + else { + setReasons.push(target) + } + if ((target === AlternativeDataSharingPlanReasons.VALUES.isInformedConsentProcessesInadequate) && removed) { + if (setReasons.includes(AlternativeDataSharingPlanReasons.VALUES.legalRestrictions)) { + setReasons = [AlternativeDataSharingPlanReasons.VALUES.legalRestrictions] + } + else { + setReasons = [] + } + } + setStudyPropertyByKey(study, setStudy, { isValid: true }, new AlternativeDataSharingPlanReasons(setReasons)) + } + } + + return ( +
+ {( + NihAnvilUse.requiresNIHAdministrativeInformation(getStudyPropertyValueByKey(study, NihAnvilUse.key) as string | undefined) && ( + <> +

NIH Data Management & Sharing Policy Details

+ { + setStudyPropertyByKey(study, setStudy, { isValid: true }, new AlternativeDataSharingPlan(value)) + if (!value) { + setStudy((val) => { + const newVal = cloneDeep(val) + removeStudyPropertiesByKeys(newVal, new Set([AlternativeDataSharingPlanReasons.key, + AlternativeDataSharingPlanExplanation.key, + AlternativeDataSharingPlanDataSubmitted.key, + AlternativeDataSharingPlanDataReleased.key])) + unset(study, 'alternativeDataSharingPlanFile') + return newVal + }) + } + }} + /> + {(getStudyPropertyValueByKey(study, AlternativeDataSharingPlan.key) === true) && ( +
+

Please mark the reasons for which you are requesting an Alternative Data Sharing plan (check all that apply)*

+ + + {(getStudyPropertyValueByKey(study, AlternativeDataSharingPlanReasons.key) as string[] ?? []).includes(AlternativeDataSharingPlanReasons.VALUES.isInformedConsentProcessesInadequate) && ( +
+ + + + + + +
+ )} + {generateStudyPropertyFormTextField(study, setStudy, new AlternativeDataSharingPlanExplanation(), [FormValidators.REQUIRED])} + { + setFiles((val: FileProperty) => { + const newFiles = cloneDeep(val) + set(newFiles, key, value) + return newFiles + }) + }} + /> + { + setStudyPropertyByKey(study, setStudy, input, new AlternativeDataSharingPlanDataSubmitted(input.value as string)) + }} + /> + {generateStudyPropertyYesNoField(study, setStudy, new AlternativeDataSharingPlanDataReleased())} +
+ )} + + ))} +
+ ) +} diff --git a/src/pages/data_submission/v2/v2-common-functions.tsx b/src/pages/data_submission/v2/v2-common-functions.tsx index 5493e15d23..366937da53 100644 --- a/src/pages/data_submission/v2/v2-common-functions.tsx +++ b/src/pages/data_submission/v2/v2-common-functions.tsx @@ -3,29 +3,63 @@ import { Study, StudyProperty, StringStudyProperty, - DateStudyProperty, + DateStudyProperty, BooleanStudyProperty, } from 'src/pages/data_submission/v2/v2-models' -import { FormField } from 'src/components/forms/forms' +import { FormField, FormFieldTypes } from 'src/components/forms/forms' +import { cloneDeep, set } from 'lodash' -export type MasterChangeHandler = ({ key, value, isValid }: { key: string, value: unknown, isValid: boolean }) => void +export type MasterChangeHandler = ({ key, value, isValid, remove }: { key: string, value: unknown, isValid: boolean, remove?: boolean }) => void -export const generateFormTextField = (formData: Study, onChange: MasterChangeHandler, studyProperty: StringStudyProperty, validators: Array = []) => { +export const generateStudyPropertyYesNoField = (formData: Study, setStudy: React.Dispatch>, studyProperty: BooleanStudyProperty) => { + return ( + { + studyProperty.value = value + setStudyPropertyByKey(formData, setStudy, { isValid: true }, studyProperty) + }} + /> + ) +} + +export const generateStudyPropertyFormTextField = (formData: Study, setStudy: React.Dispatch>, studyProperty: StringStudyProperty, validators: Array = []) => { return ( { studyProperty.value = input.value as string - setStudyPropertyByKey(formData, onChange, input, studyProperty) + setStudyPropertyByKey(formData, setStudy, input, studyProperty) + }} + /> + ) +} + +export const generateStudyInputFormTextField = (setStudy: React.Dispatch>, id: string, initialValue: string | undefined, title: string, placeholder: string, validators: Array = []) => { + return ( + { + setStudy((val: Study) => { + const newForm = cloneDeep(val) + return set(newForm, input.key, input.value) + }) }} /> ) } -export const generateFormDateField = (formData: Study, onChange: MasterChangeHandler, studyProperty: DateStudyProperty, validators: Array = [], style: unknown = {}) => { +export const generateStudyPropertyFormDateField = (formData: Study, setStudy: React.Dispatch>, studyProperty: DateStudyProperty, validators: Array = [], style: unknown = {}) => { return ( { studyProperty.value = input.value as Date - setStudyPropertyByKey(formData, onChange, input, studyProperty) + setStudyPropertyByKey(formData, setStudy, input, studyProperty) }} /> ) } -export const setStudyPropertyByKey = (formData: Study, onChange: MasterChangeHandler, input: { isValid: boolean }, propertyInstance: StudyProperty) => { +export const setStudyPropertyByKey = (formData: Study, setStudy: React.Dispatch>, input: { isValid: boolean }, propertyInstance: StudyProperty) => { if (!input.isValid) { return } - formData.properties = formData.properties ?? [] - const filteredProperty = formData.properties.find(prop => prop.key === propertyInstance.key) + const studyToUpdate = cloneDeep(formData) + studyToUpdate.properties = studyToUpdate.properties ?? [] + const filteredProperty = studyToUpdate.properties.find(prop => prop.key === propertyInstance.key) if (filteredProperty) { filteredProperty.value = propertyInstance.value } else { - formData.properties.push(propertyInstance.toJSON() as StudyProperty) + studyToUpdate.properties.push(propertyInstance.toJSON() as StudyProperty) + } + setStudy(() => { + return studyToUpdate + }) +} + +export const removeStudyPropertiesByKeys = (study: Study, keys: Set) => { + if (!study?.properties || !Array.isArray(study.properties)) { + return study + } + else { + const arr: StudyProperty[] = study.properties + let i = 0 + while (i < arr.length) { + if (keys.has(arr[i].key)) { + arr.splice(i, 1) + } + else { + ++i + } + } } - onChange({ key: 'properties', value: formData.properties, isValid: input.isValid }) } -export const getStudyPropertyByKey = (formData: Study, key: string) => { +export const getStudyPropertyValueByKey = (formData: Study, key: string): unknown => { if (!formData?.properties) { return undefined } diff --git a/src/pages/data_submission/v2/v2-models.ts b/src/pages/data_submission/v2/v2-models.ts deleted file mode 100644 index 9267463b41..0000000000 --- a/src/pages/data_submission/v2/v2-models.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Dataset, FileStorageObject } from 'src/types/model' - -export type StudyPropertyType = 'Boolean' | 'String' | 'Number' | 'Date' | 'Json' - -export class StudyProperty { - key: string - studyPropertyId?: number - studyId?: number - type: StudyPropertyType - value?: unknown - - constructor(key: string, type: StudyPropertyType, value?: unknown, studyId?: number, studyPropertyId?: number) { - this.key = key - this.studyId = studyId - this.studyPropertyId = studyPropertyId - this.type = type - this.value = value - } - - toJSON() { - const obj = { - key: this.key, - value: this.value, - studyId: this.studyId, - studyPropertyId: this.studyPropertyId, - } - if (!obj.studyId) { - delete obj.studyId - } - if (!obj.studyPropertyId) { - delete obj.studyPropertyId - } - return obj - } -} - -export class StringStudyProperty extends StudyProperty { - fieldTitle: string - fieldPlaceholderText: string - constructor(key: string, fieldTitle: string, fieldPlaceholderText: string, value?: unknown, studyId?: number, studyPropertyId?: number) { - super(key, 'String', value, studyId, studyPropertyId) - this.fieldPlaceholderText = fieldPlaceholderText - this.fieldTitle = fieldTitle - } -} - -export class DateStudyProperty extends StudyProperty { - fieldTitle: string - fieldPlaceholderText: string - constructor(key: string, fieldTitle: string, fieldPlaceholderText: string, value?: unknown, studyId?: number, studyPropertyId?: number) { - super(key, 'Date', value, studyId, studyPropertyId) - this.fieldPlaceholderText = fieldPlaceholderText - this.fieldTitle = fieldTitle - } -} - -export class NihGrantContractNumber extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('nihGrantContractNumber', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class SubmittingToAnvil extends StudyProperty { - constructor(value: boolean, studyId?: number, studyPropertyId?: number) { - super('submittingToAnvil', 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanTargetPublicReleaseDate extends DateStudyProperty { - constructor(value?: Date, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanTargetPublicReleaseDate', 'Target Public Release Date', 'Please enter date (YYYY-MM-DD)', value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanFileName extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanFileName', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class DbGaPStudyRegistrationName extends StringStudyProperty { - constructor(value?: string, studyId?: number, studyPropertyId?: number) { - super('dbGaPStudyRegistrationName', 'dbGaP Study Registration Name', 'Name', value, studyId, studyPropertyId) - } -} - -export class PiInstitution extends StudyProperty { - constructor(value: number, studyId?: number, studyPropertyId?: number) { - super('piInstitution', 'Number' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class DataCustodianEmail extends StudyProperty { - constructor(value: string[], studyId?: number, studyPropertyId?: number) { - super('dataCustodianEmail', 'Json' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanTargetDeliveryDate extends DateStudyProperty { - constructor(value?: Date, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanTargetDeliveryDate', 'Target Delivery Date', 'Please enter date (YYYY-MM-DD)', value, studyId, studyPropertyId) - } -} - -export class PhenotypeIndication extends StringStudyProperty { - constructor(value?: string, studyId?: number, studyPropertyId?: number) { - super('phenotypeIndication', 'Phenotype/Indication Studied', 'Enter the "Phenotype/Indication studied"', value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanExplanation extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanExplanation', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanReasons extends StudyProperty { - constructor(value: string[], studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanReasons', 'Json' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class Species extends StringStudyProperty { - constructor(value?: string, studyId?: number, studyPropertyId?: number) { - super('species', 'Species', 'Species', value, studyId, studyPropertyId) - } -} - -export class NihICsSupportingStudy extends StudyProperty { - constructor(value: string[], studyId?: number, studyPropertyId?: number) { - super('nihICsSupportingStudy', 'Json' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanDataSubmitted extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanDataSubmitted', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanControlledOpenAccess extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanControlledOpenAccess', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class NihInstitutionCenterSubmission extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('nihInstitutionCenterSubmission', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class MultiCenterStudy extends StudyProperty { - constructor(value: boolean, studyId?: number, studyPropertyId?: number) { - super('multiCenterStudy', 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlan extends StudyProperty { - constructor(value: boolean, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlan', 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class DbGaPPhsID extends StringStudyProperty { - constructor(value?: string, studyId?: number, studyPropertyId?: number) { - super('dbGaPPhsID', 'dbGaP phs ID', 'Enter phs ID', value, studyId, studyPropertyId) - } -} - -export class EmbargoReleaseDate extends DateStudyProperty { - constructor(value?: Date, studyId?: number, studyPropertyId?: number) { - super('embargoReleaseDate', 'Embargo Release Date', 'YYYY-MM-DD', value, studyId, studyPropertyId) - } -} - -export class StudyType extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('studyType', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class SequencingCenter extends StringStudyProperty { - constructor(value?: string, studyId?: number, studyPropertyId?: number) { - super('sequencingCenter', 'Sequencing Center', 'Name', value, studyId, studyPropertyId) - } -} - -export class AlternativeDataSharingPlanDataReleased extends StudyProperty { - constructor(value: boolean, studyId?: number, studyPropertyId?: number) { - super('alternativeDataSharingPlanDataReleased', 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class ControlledAccessRequiredForGenomicSummaryResultsGSR extends StudyProperty { - constructor(value: boolean, studyId?: number, studyPropertyId?: number) { - super('controlledAccessRequiredForGenomicSummaryResultsGSR', 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class NihProgramOfficerName extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('nihProgramOfficerName', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class NihAnvilUse extends StudyProperty { - static readonly YES_NHGRI_YES_PHS_ID = 'I am NHGRI funded and I have a dbGaP PHS ID already' - static readonly YES_NHGRI_NO_PHS_ID = 'I am NHGRI funded and I do not have a dbGaP PHS ID' - static readonly NO_NHGRI_YES_ANVIL = 'I am not NHGRI funded but I am seeking to submit data to AnVIL' - static readonly NO_NHGRI_NO_ANVIL = 'I am not NHGRI funded and do not plan to store data in AnVIL' - static readonly NIH_ANVIL_USE_RADIOGROUP_OPTIONS = [ - { text: NihAnvilUse.YES_NHGRI_YES_PHS_ID, name: NihAnvilUse.YES_NHGRI_YES_PHS_ID }, - { text: NihAnvilUse.YES_NHGRI_NO_PHS_ID, name: NihAnvilUse.YES_NHGRI_NO_PHS_ID }, - { text: NihAnvilUse.NO_NHGRI_YES_ANVIL, name: NihAnvilUse.NO_NHGRI_YES_ANVIL }, - { text: NihAnvilUse.NO_NHGRI_NO_ANVIL, name: NihAnvilUse.NO_NHGRI_NO_ANVIL }, - ] - - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('nihAnvilUse', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class NihGenomicProgramAdministratorName extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('nihGenomicProgramAdministratorName', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class CollaboratingSites extends StudyProperty { - constructor(value: string[], studyId?: number, studyPropertyId?: number) { - super('collaboratingSites', 'Json' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export class ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation extends StudyProperty { - constructor(value: string, studyId?: number, studyPropertyId?: number) { - super('controlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation', 'String' as StudyPropertyType, value, studyId, studyPropertyId) - } -} - -export interface Study { - studyId?: number - uuid?: string - name?: string - description?: string - dataTypes?: string[] - piName?: string - publicVisibility?: boolean - datasetIds?: number[] - datasets?: Dataset[] - properties?: StudyProperty[] - alternativeDataSharingPlan?: FileStorageObject - createDate?: string // Date? - createUserId?: number - updateDate?: string // Date? - updateUserId?: number -} diff --git a/src/pages/data_submission/v2/v2-models.tsx b/src/pages/data_submission/v2/v2-models.tsx new file mode 100644 index 0000000000..8318894329 --- /dev/null +++ b/src/pages/data_submission/v2/v2-models.tsx @@ -0,0 +1,342 @@ +import { Dataset, FileStorageObject } from 'src/types/model' +import { StudyType } from 'src/components/forms/StudyType' +import React from 'react' + +export type StudyPropertyType = 'Boolean' | 'String' | 'Number' | 'Date' | 'Json' + +export class StudyProperty { + key: string + studyPropertyId?: number + studyId?: number + type: StudyPropertyType + value?: unknown + + constructor(key: string, type: StudyPropertyType, value?: unknown, studyId?: number, studyPropertyId?: number) { + this.key = key + this.studyId = studyId + this.studyPropertyId = studyPropertyId + this.type = type + this.value = value + } + + toJSON() { + const obj = { + key: this.key, + value: this.value, + studyId: this.studyId, + studyPropertyId: this.studyPropertyId, + } + if (!obj.studyId) { + delete obj.studyId + } + if (!obj.studyPropertyId) { + delete obj.studyPropertyId + } + return obj + } +} + +export class StringStudyProperty extends StudyProperty { + fieldTitle: string + fieldPlaceholderText: string + constructor(key: string, fieldTitle: string, fieldPlaceholderText: string, value?: unknown, studyId?: number, studyPropertyId?: number) { + super(key, 'String', value, studyId, studyPropertyId) + this.fieldPlaceholderText = fieldPlaceholderText + this.fieldTitle = fieldTitle + } +} + +export class BooleanStudyProperty extends StudyProperty { + fieldTitle: string | React.JSX.Element + constructor(key: string, fieldTitle: string | React.JSX.Element, value?: boolean | undefined, studyId?: number, studyPropertyId?: number) { + super(key, 'Boolean', value, studyId, studyPropertyId) + this.fieldTitle = fieldTitle + } +} + +export class DateStudyProperty extends StudyProperty { + fieldTitle: string + fieldPlaceholderText: string + constructor(key: string, fieldTitle: string, fieldPlaceholderText: string, value?: unknown, studyId?: number, studyPropertyId?: number) { + super(key, 'Date', value, studyId, studyPropertyId) + this.fieldPlaceholderText = fieldPlaceholderText + this.fieldTitle = fieldTitle + } +} + +export class NihGrantContractNumber extends StringStudyProperty { + static readonly key = 'nihGrantContractNumber' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(NihGrantContractNumber.key, 'NIH Grant or Contract Number', 'Enter the Grant or Contract Number', value, studyId, studyPropertyId) + } +} + +export class SubmittingToAnvil extends StudyProperty { + static readonly key = 'submittingToAnvil' + constructor(value: boolean, studyId?: number, studyPropertyId?: number) { + super(SubmittingToAnvil.key, 'Boolean' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanTargetPublicReleaseDate extends DateStudyProperty { + static readonly key = 'alternativeDataSharingPlanTargetPublicReleaseDate' + constructor(value?: Date, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanTargetPublicReleaseDate.key, 'Target Public Release Date', 'Please enter date (YYYY-MM-DD)', value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanFileName extends StudyProperty { + static readonly key = 'alternativeDataSharingPlanFileName' + constructor(value: string, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanFileName.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class DbGaPStudyRegistrationName extends StringStudyProperty { + static readonly key = 'dbGaPStudyRegistrationName' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(DbGaPStudyRegistrationName.key, 'dbGaP Study Registration Name', 'Name', value, studyId, studyPropertyId) + } +} + +export class PiInstitution extends StudyProperty { + static readonly key = 'piInstitution' + static readonly fieldTitle = 'Principal Investigator Institution' + constructor(value: number, studyId?: number, studyPropertyId?: number) { + super(PiInstitution.key, 'Number' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class DataCustodianEmail extends StudyProperty { + static readonly key = 'dataCustodianEmail' + constructor(value: string[], studyId?: number, studyPropertyId?: number) { + super(DataCustodianEmail.key, 'Json' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanTargetDeliveryDate extends DateStudyProperty { + static readonly key = 'alternativeDataSharingPlanTargetDeliveryDate' + constructor(value?: Date, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanTargetDeliveryDate.key, 'Target Delivery Date', 'Please enter date (YYYY-MM-DD)', value, studyId, studyPropertyId) + } +} + +export class PhenotypeIndication extends StringStudyProperty { + static readonly key = 'phenotypeIndication' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(PhenotypeIndication.key, 'Phenotype/Indication Studied', 'Enter the "Phenotype/Indication studied"', value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanExplanation extends StringStudyProperty { + static readonly key = 'alternativeDataSharingPlanExplanation' + static readonly fieldTitle = 'Explanation for request' + static readonly fieldPlaceholderText = 'Enter the explanation for the request' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanExplanation.key, AlternativeDataSharingPlanExplanation.fieldTitle, AlternativeDataSharingPlanExplanation.fieldPlaceholderText, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanReasons extends StudyProperty { + static readonly key = 'alternativeDataSharingPlanReasons' + static readonly VALUES = { + legalRestrictions: 'Legal Restrictions', + isInformedConsentProcessesInadequate: 'Informed consent processes are inadequate to support data for sharing for the following reasons:', + consentFormsUnavailable: 'The consent forms are unavailable or non-existent for samples collected after January 25, 2015', + consentProcessDidNotAddressFutureUseOrBroadSharing: 'The consent process did not specifically address future use or broad data sharing for samples collected after January 25, 2015', + consentProcessInadequatelyAddressesRisk: 'The consent process inadequately addresses risks related to future use or broad data sharing for samples collected after January 25, 2015', + consentProcessPrecludesFutureUseOrBroadSharing: 'The consent process specifically precludes future use or broad data sharing (including a statement that use of data will be limited to the original researchers)', + otherInformedConsentLimitationsOrConcerns: 'Other informed consent limitations or concerns', + otherReasonForRequest: 'Other', + } + + constructor(value: string[], studyId?: number, studyPropertyId?: number) { + super('alternativeDataSharingPlanReasons', 'Json' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class Species extends StringStudyProperty { + static readonly key = 'species' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(Species.key, 'Species', 'Species', value, studyId, studyPropertyId) + } +} + +export class NihICsSupportingStudy extends StudyProperty { + static readonly key = 'nihICsSupportingStudy' + static readonly fieldTitle = 'NIH ICs Supporting the Study' + static readonly fieldPlaceholderText = 'Institute/Center Name' + constructor(value: string[], studyId?: number, studyPropertyId?: number) { + super(NihICsSupportingStudy.key, 'Json' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanDataSubmitted extends StudyProperty { + static readonly key = 'alternativeDataSharingPlanDataSubmitted' + static readonly fieldTitle = 'Data will be submitted:' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanDataSubmitted.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanControlledOpenAccess extends StudyProperty { + static readonly key = 'alternativeDataSharingPlanControlledOpenAccess' + constructor(value: string, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanControlledOpenAccess.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class NihInstitutionCenterSubmission extends StudyProperty { + static readonly key = 'nihInstitutionCenterSubmission' + static readonly fieldTitle = 'NIH Institute/Center for Submission' + static readonly fieldPlaceholderText = 'Institute/Center Name' + constructor(value: string, studyId?: number, studyPropertyId?: number) { + super(NihInstitutionCenterSubmission.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class MultiCenterStudy extends BooleanStudyProperty { + static readonly key = 'multiCenterStudy' + static readonly fieldTitle = 'Is this a multi-center study?' + constructor(value?: boolean, studyId?: number, studyPropertyId?: number) { + super(MultiCenterStudy.key, MultiCenterStudy.fieldTitle, value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlan extends BooleanStudyProperty { + static readonly key = 'alternativeDataSharingPlan' + static readonly fieldTitle = ( + + Are you requesting an Alternative Data Sharing Plan + {' '} + + (info) + + {' '} + for samples that cannot be shared through a public repository or database? + + ) + + constructor(value?: boolean, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlan.key, AlternativeDataSharingPlan.fieldTitle, value, studyId, studyPropertyId) + } +} + +export class DbGaPPhsID extends StringStudyProperty { + static readonly key = 'dbGaPPhsID' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(DbGaPPhsID.key, 'dbGaP phs ID', 'Enter phs ID', value, studyId, studyPropertyId) + } +} + +export class EmbargoReleaseDate extends DateStudyProperty { + static readonly key = 'embargoReleaseDate' + constructor(value?: Date, studyId?: number, studyPropertyId?: number) { + super(EmbargoReleaseDate.key, 'Embargo Release Date', 'YYYY-MM-DD', value, studyId, studyPropertyId) + } +} + +export class StudyTypeProperty extends StudyProperty { + static readonly key = 'studyType' + static readonly fieldTitle = 'Study Type' + static readonly fieldPlaceholderText = 'Select a Study Type' + static readonly STUDY_TYPE_OPTIONS = StudyType.NAME_VALUES + + constructor(value: string, studyId?: number, studyPropertyId?: number) { + super(StudyTypeProperty.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class SequencingCenter extends StringStudyProperty { + static readonly key = 'sequencingCenter' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(SequencingCenter.key, 'Sequencing Center', 'Name', value, studyId, studyPropertyId) + } +} + +export class AlternativeDataSharingPlanDataReleased extends BooleanStudyProperty { + static readonly key = 'alternativeDataSharingPlanDataReleased' + static readonly fieldTitle = 'Data to be released will meet the timeframes specified in the NHGRI Guidance for Data Submission and Data Release' + constructor(value?: boolean, studyId?: number, studyPropertyId?: number) { + super(AlternativeDataSharingPlanDataReleased.key, AlternativeDataSharingPlanDataReleased.fieldTitle, value, studyId, studyPropertyId) + } +} + +export class ControlledAccessRequiredForGenomicSummaryResultsGSR extends BooleanStudyProperty { + static readonly key = 'controlledAccessRequiredForGenomicSummaryResultsGSR' + static readonly fieldTitle = 'Is controlled access required for genomic summary results (GSR)?' + constructor(value?: boolean, studyId?: number, studyPropertyId?: number) { + super(ControlledAccessRequiredForGenomicSummaryResultsGSR.key, ControlledAccessRequiredForGenomicSummaryResultsGSR.fieldTitle, value, studyId, studyPropertyId) + } +} + +export class NihProgramOfficerName extends StringStudyProperty { + static readonly key = 'nihProgramOfficerName' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(NihProgramOfficerName.key, 'NIH Program Officer Name', 'Enter the NIH Program Officer\'s Name', value, studyId, studyPropertyId) + } +} + +export class NihAnvilUse extends StudyProperty { + static readonly key = 'nihAnvilUse' + static readonly YES_NHGRI_YES_PHS_ID = 'I am NHGRI funded and I have a dbGaP PHS ID already' + static readonly YES_NHGRI_NO_PHS_ID = 'I am NHGRI funded and I do not have a dbGaP PHS ID' + static readonly NO_NHGRI_YES_ANVIL = 'I am not NHGRI funded but I am seeking to submit data to AnVIL' + static readonly NO_NHGRI_NO_ANVIL = 'I am not NHGRI funded and do not plan to store data in AnVIL' + static readonly NIH_ANVIL_USE_RADIOGROUP_OPTIONS = [ + { text: NihAnvilUse.YES_NHGRI_YES_PHS_ID, name: NihAnvilUse.YES_NHGRI_YES_PHS_ID }, + { text: NihAnvilUse.YES_NHGRI_NO_PHS_ID, name: NihAnvilUse.YES_NHGRI_NO_PHS_ID }, + { text: NihAnvilUse.NO_NHGRI_YES_ANVIL, name: NihAnvilUse.NO_NHGRI_YES_ANVIL }, + { text: NihAnvilUse.NO_NHGRI_NO_ANVIL, name: NihAnvilUse.NO_NHGRI_NO_ANVIL }, + ] + + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(NihAnvilUse.key, 'String' as StudyPropertyType, value, studyId, studyPropertyId) + } + + static requiresNIHAdministrativeInformation(value: string | null | undefined): boolean { + if (!value) { + return false + } + return [NihAnvilUse.YES_NHGRI_YES_PHS_ID, NihAnvilUse.YES_NHGRI_NO_PHS_ID, NihAnvilUse.NO_NHGRI_YES_ANVIL].includes(value) + } +} + +export class NihGenomicProgramAdministratorName extends StringStudyProperty { + static readonly key = 'nihGenomicProgramAdministratorName' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(NihGenomicProgramAdministratorName.key, 'NIH Genomic Program Administrator Name', 'Enter the NIH Genomic Program Administrator\'s name ', value, studyId, studyPropertyId) + } +} + +export class CollaboratingSites extends StudyProperty { + static readonly key = 'collaboratingSites' + constructor(value: string[], studyId?: number, studyPropertyId?: number) { + super(CollaboratingSites.key, 'Json' as StudyPropertyType, value, studyId, studyPropertyId) + } +} + +export class ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation extends StringStudyProperty { + static readonly key = 'controlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation' + constructor(value?: string, studyId?: number, studyPropertyId?: number) { + super(ControlledAccessRequiredForGenomicSummaryResultsGSRRequiredExplanation.key, 'If yes, explain why controlled access is needed for GSR.', 'Enter explanation here', value, studyId, studyPropertyId) + } +} + +export interface Study { + studyId?: number + uuid?: string + name?: string + description?: string + dataTypes?: string[] + piName?: string + publicVisibility?: boolean + datasetIds?: number[] + datasets?: Dataset[] + properties?: StudyProperty[] + alternativeDataSharingPlan?: FileStorageObject + createDate?: string // Date? + createUserId?: number + updateDate?: string // Date? + updateUserId?: number +} diff --git a/src/pages/user_profile/AffiliationAndRoles.tsx b/src/pages/user_profile/AffiliationAndRoles.tsx index c384af2dec..c394b4c91e 100644 --- a/src/pages/user_profile/AffiliationAndRoles.tsx +++ b/src/pages/user_profile/AffiliationAndRoles.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { Institution as InstitutionAPI } from '../../libs/ajax/Institution' import { Notifications } from '../../libs/utils' -import { DuosUser, Institution } from '../../types/model' +import { DuosUser, InstitutionInterface } from '../../types/model' interface AffiliationAndRoleProps { readonly user: DuosUser @@ -9,7 +9,7 @@ interface AffiliationAndRoleProps { export default function AffiliationAndRole(props: AffiliationAndRoleProps) { const { user } = props - const [institution, setInstitution] = useState() + const [institution, setInstitution] = useState() const [roles, setRoles] = useState('') useEffect(() => { @@ -20,7 +20,7 @@ export default function AffiliationAndRole(props: AffiliationAndRoleProps) { const allRoles = user?.roles?.map(role => role.name).join(', ') setRoles(allRoles) if (user?.institutionId) { - const institution: Institution = await InstitutionAPI.getById(user.institutionId) + const institution: InstitutionInterface = await InstitutionAPI.getById(user.institutionId) if (institution) { setInstitution(institution) } diff --git a/src/routing/AppRoutes.tsx b/src/routing/AppRoutes.tsx index a7b8b79f50..61f5c96942 100644 --- a/src/routing/AppRoutes.tsx +++ b/src/routing/AppRoutes.tsx @@ -101,8 +101,10 @@ const AppRoutes = (props: AppRoutesProps) => { }> } /> } /> - }> - } /> + }> + }> + } /> + } /> diff --git a/src/types/model.ts b/src/types/model.ts index eedc3bc38c..532ebbcae0 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -123,7 +123,7 @@ export interface LibraryCard { export type OrganizationType = 'For-Profit' | 'Nonprofit' -export interface Institution { +export interface InstitutionInterface { id: number name: string itDirectorName?: string