Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ beforeEach(() => {
describe('GeneralStudyInformation - Tests', () => {
it('should mount with all the fields', () => {
cy.mount(<GeneralStudyInformation {...propCopy} />)
cy.get('.formField-container').should('have.length', 13)
cy.get('.formField-container').should('have.length', 14)

cy.get('.formField-name').should('have.length', 1)
cy.get('.formField-studyType').should('have.length', 1)
Expand All @@ -33,6 +33,7 @@ describe('GeneralStudyInformation - Tests', () => {
cy.get('#alternativeDataSharingPlanTargetDeliveryDate').should('exist')
cy.get('#alternativeDataSharingPlanTargetPublicReleaseDate').should('exist')
cy.get('.formField-publicVisibility').should('have.length', 1)
cy.get('.formField-throughBioId').should('have.length', 1)
})

it('should allow edit in all fields', () => {
Expand Down Expand Up @@ -66,5 +67,7 @@ describe('GeneralStudyInformation - Tests', () => {
cy.get('@setStudySpy').its('callCount').should('eq', 63)
cy.get('.formField-tags').type('tag1{enter}tag2{enter}')
cy.get('@setStudySpy').its('callCount').should('eq', 65)
cy.get('.formField-throughBioId').type('test-bio-id')
cy.get('@setStudySpy').its('callCount').should('eq', 76)
})
})
27 changes: 26 additions & 1 deletion cypress/component/Forms/formValidation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { greaterThanZeroValidator, requiredValidator, urlValidator } from 'src/components/forms/formValidation'
import {
greaterThanZeroValidator,
NotUrlValidator,
requiredValidator,
urlValidator,
} from 'src/components/forms/formValidation'

describe('Form Validator tests', () => {
describe('Validate number greater than zero tests', () => {
Expand Down Expand Up @@ -49,4 +54,24 @@ describe('Form Validator tests', () => {
expect(urlValidator.isValid(null)).to.be.equal(false)
})
})
describe('Validate \'not url\' field tests', () => {
it('Non-url string should validate to true', () => {
expect(NotUrlValidator.isValid('hello! I am a test')).to.be.equal(true)
})
it('Valid URL should validate to false', () => {
expect(NotUrlValidator.isValid('https://www.broadinstitute.org')).to.be.equal(false)
})
it('Empty string should validate to true', () => {
expect(NotUrlValidator.isValid('')).to.be.equal(true)
})
it('Whitespace string should validate to true', () => {
expect(NotUrlValidator.isValid(' ')).to.be.equal(true)
})
it('undefined should validate to true', () => {
expect(NotUrlValidator.isValid(undefined)).to.be.equal(true)
})
it('null should validate to true', () => {
expect(NotUrlValidator.isValid(null)).to.be.equal(true)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { extractThroughBioId } from 'src/pages/data_submission/v2/v2-common-functions'

describe('extractThroughBioId', () => {
const validUrls = [
['https://through.bio/abc123', 'abc123'],
['https://through.bio/xyz', 'xyz'],
['https://through.bio/abc/def', 'abc/def'],
]
validUrls.forEach(([input, expected]) => {
it(`extracts ID "${expected}" from "${input}"`, () => {
expect(extractThroughBioId(input)).to.equal(expected)
})
})

const invalidUrls = [
'https://example.com/abc123',
'https://throughbio.com/abc',
]
invalidUrls.forEach((input) => {
it(`returns empty string for invalid URL "${input}"`, () => {
expect(extractThroughBioId(input)).to.equal('')
})
})

const nonUrlStrings = [
[' myid ', 'myid'],
['anotherId', 'anotherId'],
]
nonUrlStrings.forEach(([input, expected]) => {
it(`returns trimmed input "${expected}" for "${input}"`, () => {
expect(extractThroughBioId(input)).to.equal(expected)
})
})

const emptyInputs = [
'',
' ',
]
emptyInputs.forEach((input) => {
it(`returns empty string for empty input "${input}"`, () => {
expect(extractThroughBioId(input)).to.equal('')
})
})
})
24 changes: 21 additions & 3 deletions src/components/forms/formValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export const urlValidator = {
msg: 'Please enter a valid url (e.g., https://duos.org)',
}

export const NotUrlValidator = {
id: 'notUri',
isValid: (val) => {
return !validURLObject(val)
},
msg: 'Please enter a value that is not a url',
}

export const emailValidator = {
id: 'email',
isValid: isEmailAddress,
Expand Down Expand Up @@ -74,7 +82,17 @@ export const greaterThanZeroValidator = {
msg: 'Please enter a number greater than zero',
}

const validators = [requiredValidator, urlValidator, emailValidator, emailDomainValidator, dateValidator, dayJSValidator, uniqueValidator, greaterThanZeroValidator]
const validators = [
requiredValidator,
urlValidator,
NotUrlValidator,
emailValidator,
emailDomainValidator,
dateValidator,
dayJSValidator,
uniqueValidator,
greaterThanZeroValidator,
]

/**
* Validates the form value
Expand All @@ -94,7 +112,7 @@ export const validateFormValue = (formValue, validators) => {
const failedValidators = []

validators?.forEach((validator) => {
let failed = false
let failed
if (isArray(formValue)) {
failed = formValue.some((val) => {
return !validator.isValid(val)
Expand All @@ -116,7 +134,7 @@ export const validateFormValue = (formValue, validators) => {
}

/**
* Gives a human readable validation message. Gives generic message if the validator cannot be found.
* Gives a human-readable validation message. Gives generic message if the validator cannot be found.
*
* @param {string} failedValidator The id of the failed validator, e.g. 'required'
* @returns Human readable message, e.g., 'Please enter a value'.
Expand Down
2 changes: 2 additions & 0 deletions src/components/forms/forms.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
emailDomainValidator,
emailValidator,
isValid,
NotUrlValidator,
requiredValidator,
urlValidator,
} from './formValidation'
Expand Down Expand Up @@ -197,6 +198,7 @@ export const FormFieldTypes = {
export const FormValidators = {
REQUIRED: requiredValidator,
URL: urlValidator,
NOTURL: NotUrlValidator,
EMAIL: emailValidator,
EMAILDOMAIN: emailDomainValidator,
DATE: dateValidator,
Expand Down
45 changes: 40 additions & 5 deletions src/pages/data_submission/v2/GeneralStudyInformation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import { FormFieldTypes, FormField, FormValidators } from 'src/components/forms/forms'
import {
Study,
Expand All @@ -9,8 +9,10 @@ import {
AlternativeDataSharingPlanTargetDeliveryDate,
AlternativeDataSharingPlanTargetPublicReleaseDate,
StudyData,
ThroughBioId,
} from 'src/pages/data_submission/v2/v2-models'
import {
extractThroughBioId,
generateStudyInputFormTextField,
generateStudyPropertyFormDateField,
generateStudyPropertyFormTextField,
Expand All @@ -19,17 +21,25 @@ import {
} from 'src/pages/data_submission/v2/v2-common-functions'
import { DataTypes } from 'src/components/forms/DataTypes'
import { set } from 'lodash'
import { ValidationError } from 'src/pages/dar_application/FormValidationState'

interface Validation {
throughBioId?: ValidationError
}

export interface GeneralStudyInformationProps {
study: Study
setStudy: React.Dispatch<React.SetStateAction<Study>>
}

export const GeneralStudyInformation = (props: GeneralStudyInformationProps) => {
const {
setStudy,
study,
} = props
const { setStudy, study } = props
const [validation, setValidation] = useState<Validation>({})

const throughBioLink = React.useMemo(() => {
const id = getStudyPropertyValueByKey(study, 'throughBioId')
return typeof id === 'string' && id.trim() !== '' ? `https://through.bio/${id}` : undefined
}, [study])

const onChange = ({ key, value }: { key: string, value: unknown, isValid: boolean }) => {
setStudy((val: Study) => {
Expand Down Expand Up @@ -143,6 +153,31 @@ export const GeneralStudyInformation = (props: GeneralStudyInformationProps) =>
defaultValue={study?.publicVisibility}
onChange={onChange}
/>
<FormField
id={ThroughBioId.key}
title={ThroughBioId.fieldTitle}
helpText={
throughBioLink
? <a href={throughBioLink} target="_blank" rel="noopener noreferrer">{throughBioLink}</a>
: 'Through.bio/'
}
type={FormFieldTypes.TEXT}
placeholder={ThroughBioId.fieldPlaceholderText}
defaultValue={getStudyPropertyValueByKey(study, ThroughBioId.key)}
validation={validation.throughBioId}
onChange={(input: { key: string, value: string, isValid: boolean }) => {
const id = extractThroughBioId(input.value)
if (!id && input.value) {
setValidation({
...validation,
throughBioId: { valid: false, failed: ['notUri'] },
})
return
}
setValidation({ ...validation, throughBioId: undefined })
setStudyPropertyByKey(study, setStudy, { isValid: !!id }, new ThroughBioId(id))
}}
/>
</div>
)
}
23 changes: 22 additions & 1 deletion src/pages/data_submission/v2/v2-common-functions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import {
DataURL,
FileTypes,
NumberOfParticipants, StudyData,
DatasetData } from 'src/pages/data_submission/v2/v2-models'
DatasetData,
ThroughBioId,
} from 'src/pages/data_submission/v2/v2-models'
import { FormField, FormFieldTypes } from 'src/components/forms/forms'
import { set, isEmpty } from 'lodash'
import { Storage } from 'src/libs/storage'
Expand Down Expand Up @@ -189,6 +191,7 @@ export const studyToDatasetSchemaSubmission = (study: Study): DatasetRegistratio
piEmail: study.piEmail,
dataCustodianEmail: getStudyPropertyValueByKey(study, DataCustodianEmail.key) as string[] || undefined,
publicVisibility: study.publicVisibility || false,
throughBioId: getStudyPropertyValueByKey(study, ThroughBioId.key) as string || undefined,
nihAnvilUse: getStudyPropertyValueByKey(study, NihAnvilUse.key) as NiHAnvilUseValues || undefined,
submittingToAnvil: getStudyPropertyValueByKey(study, SubmittingToAnvil.key) as boolean || undefined,
dbGaPPhsID: getStudyPropertyValueByKey(study, DbGaPPhsID.key) as string || undefined,
Expand Down Expand Up @@ -319,3 +322,21 @@ const toTitleCase = (str: string): string => {
})
.join(' ')
}

// Extracts the Through.Bio ID from a URL or returns the input if not a URL.
// Returns an empty string if the URL is not from through.bio.
export const extractThroughBioId = (input: string): string => {
const trimmed = input.trim()
try {
const url = new URL(trimmed)
if (url.hostname === 'through.bio') {
return url.pathname.slice(1)
}
// Any other URL: return ''
return ''
}
catch {
// Not a URL: return non-empty string, else ''
return trimmed === '' ? '' : trimmed
}
}
11 changes: 11 additions & 0 deletions src/pages/data_submission/v2/v2-models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ export class NihProgramOfficerName extends StringStudyProperty {
}
}

export class ThroughBioId extends StringStudyProperty {
static readonly key = 'throughBioId'
static readonly fieldTitle = 'Through Bio ID'
static readonly fieldPlaceholderText = 'Enter the Through.Bio ID for this study, if available'
constructor(value?: string, studyId?: number, studyPropertyId?: number) {
super(ThroughBioId.key, ThroughBioId.fieldTitle, ThroughBioId.fieldPlaceholderText, 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'
Expand Down Expand Up @@ -416,6 +425,8 @@ export interface DatasetRegistrationSchemaV1 {
dataCustodianEmail?: string[]
/** @description Public Visibility of this study */
publicVisibility: boolean
/** @description Through.Bio ID for this study, if available */
throughBioId?: string
/** @enum {string} */
nihAnvilUse?: NiHAnvilUseValues
/** @description Are you planning to submit to AnVIL? */
Expand Down
Loading