diff --git a/HISTORY.rst b/HISTORY.rst index aa84a7b23..33895b20d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -39,6 +39,7 @@ End-User Summary - Improving performance of case listing (#304) - Adding shortcut buttons to phenotype annotation (#289) - Fixing issue with multiple added variants (#283) +- Implementing several usability improvements for clinvar submission editor (#286). Full Change List ================ @@ -79,6 +80,7 @@ Full Change List - Improving performance of case listing (#304) - Adding shortcut buttons to phenotype annotation (#289) - Fixing issue with multiple added variants (#283) +- Implementing several usability improvements for clinvar submission editor (#286). ------- v0.23.9 diff --git a/varfish/vueapp/src/components/SubmissionList.vue b/varfish/vueapp/src/components/SubmissionList.vue index df45f87a8..7f78dceb9 100644 --- a/varfish/vueapp/src/components/SubmissionList.vue +++ b/varfish/vueapp/src/components/SubmissionList.vue @@ -20,9 +20,10 @@ @click="onListItemClicked(item.sodar_uuid)" > {{ getSubmissionLabel(item) }} + ({{ getSubmissionIndividualsLabel(item) }})
- +
@@ -41,68 +42,89 @@

- Create a new submission by selecting one of the variants below or - - - create empty. - + Create new submissions by selecting from the variants below and clicking . + If you select no variant, a blank submission will be created.

@@ -165,7 +188,7 @@ import { mapActions, mapState } from 'vuex' import draggable from 'vuedraggable' import SubmissionEditor from './SubmissionEditor' -import { getSubmissionLabel, validConfirmed } from '@/helpers' +import { isDiseaseTerm, getSubmissionLabel, validConfirmed, removeItemAll, HPO_INHERITANCE_MODE, HPO_AGE_OF_ONSET } from '@/helpers' export default { components: { draggable, SubmissionEditor }, @@ -177,12 +200,16 @@ export default { modalIncludeFinalCausatives: true, modalIncludeAcmg3: false, modalIncludeAcmg4: true, - modalIncludeAcmg5: true + modalIncludeAcmg5: true, + individualFilter: '', + onlyAddAffected: true, + selectedSmallVariants: [] } }, computed: { ...mapState({ individuals: state => state.clinvarExport.individuals, + submissionIndividuals: state => state.clinvarExport.submissionIndividuals, submissions: state => state.clinvarExport.submissions, currentSubmissionSet: state => state.clinvarExport.currentSubmissionSet, currentSubmission: state => state.clinvarExport.currentSubmission, @@ -216,6 +243,11 @@ export default { return { ...smallVar, flags, rating, comments } }) .filter(smallVar => { + if (this.individualFilter) { + if (!smallVar.caseNames.some(s => s.includes(this.individualFilter))) { + return false + } + } if (this.modalIncludeAll) { return true } else if (this.modalIncludeComments && smallVar.comments.length > 0) { @@ -252,6 +284,25 @@ export default { getSubmissionLabel, validConfirmed, + getSubmissionIndividualsCount (item) { + return item.submission_individuals.length + }, + getSubmissionIndividualsLabel (item) { + let names = item.submission_individuals.map( + uuid => this.individuals[this.submissionIndividuals[uuid].individual].name.replace(/-N.-DNA.-....$/, '') + ) + if (names.length > 2) { + names = names.slice(0, 2) + ['...'] + } + return names.join(', ') + }, + isVariantSelected (item) { + const variantDesc = this.getVariantDesc(item) + return this.selectedSmallVariants.includes(variantDesc) + }, + getVariantDesc (item) { + return `${item.release}-${item.chromosome}-${item.start}-${item.reference}-${item.alternative}` + }, getVariantLabel (item) { return `${item.refseq_gene_symbol}:${item.refseq_hgvs_p || ''}` }, @@ -279,19 +330,34 @@ export default { this.selectCurrentSubmission(item) }) }, - onCreateEmptySubmissionClicked () { - this.createSubmissionInCurrentSubmissionSet({ - smallVariant: null, - submission: this.getEmptySubmissionData(), - individualUuids: [] - }) - this.$bvModal.hide('modal-add-submission') + onVariantClicked (smallVariant) { + const variantDesc = this.getVariantDesc(smallVariant) + if (this.selectedSmallVariants.includes(variantDesc)) { + removeItemAll(this.selectedSmallVariants, variantDesc) + } else { + this.selectedSmallVariants.push(variantDesc) + } }, /** * Clicked on an existing small variant with user annotation. */ - onCreateSubmissionClicked (smallVariant) { - this.createSubmissionInCurrentSubmissionSet(this.getSubmissionData(smallVariant)) + onCreateSubmissionClicked () { + if (!this.selectedSmallVariants.length) { + this.createSubmissionInCurrentSubmissionSet({ + smallVariant: null, + submission: this.getEmptySubmissionData(), + individualUuids: [] + }) + } else { + for (let i = 0; i < this.modalUserAnnotations.length; ++i) { + const smallVariant = this.modalUserAnnotations[i] + const variantDesc = this.getVariantDesc(smallVariant) + if (this.selectedSmallVariants.includes(variantDesc)) { + this.createSubmissionInCurrentSubmissionSet(this.getSubmissionData(smallVariant)) + } + } + this.selectedSmallVariants = [] + } this.$bvModal.hide('modal-add-submission') }, onAddSubmissionClicked () { @@ -305,10 +371,14 @@ export default { */ getSubmissionData (smallVariant) { // Get individuals that carry the variants. + const affectedNames = Object.values(this.individuals) + .filter(indiv => indiv.affected === 'yes') + .map(indiv => indiv.name) const carrierNames = Object.entries(smallVariant.genotype) .filter(kv => { + const name = kv[0] const value = kv[1] - return value.gt && value.gt.includes('1') + return value.gt && value.gt.includes('1') && (!this.onlyAddAffected || affectedNames.includes(name)) }) .map(kv => kv[0]) const individualUuids = Object.entries(this.individuals) @@ -327,6 +397,26 @@ export default { const variantGene = [smallVariant.refseq_gene_symbol] const variantHgvs = [smallVariant.refseq_hgvs_p || 'p.?'] + let ageOfOnset = '' + let inheritance = '' + const diseases = [] + for (const individualUuid of individualUuids) { + const individual = this.individuals[individualUuid] + if (individual.phenotype_terms) { + // eslint-disable-next-line camelcase + for (let { term_id, term_name } of individual.phenotype_terms) { + // eslint-disable-next-line camelcase + term_name = term_name.split(';')[0].trim() + inheritance = inheritance || HPO_INHERITANCE_MODE.get(term_id) || '' + ageOfOnset = ageOfOnset || HPO_AGE_OF_ONSET.get(term_id) || '' + // eslint-disable-next-line camelcase + if (isDiseaseTerm(term_id) && !diseases.some(x => x.term_id === term_id)) { + diseases.push({ term_id, term_name }) + } + } + } + } + const submission = { record_status: 'novel', release_status: 'public', @@ -334,8 +424,9 @@ export default { significance_description: significanceDescription, significance_last_evaluation: (new Date()).toISOString().substr(0, 10), assertion_method: Object.values(this.assertionMethods)[0].sodar_uuid, - age_of_onset: '', - inheritance: '', + age_of_onset: ageOfOnset, + diseases: diseases, + inheritance: inheritance, variant_type: 'Variation', variant_assembly: smallVariant.release, variant_chromosome: smallVariant.chromosome, @@ -398,4 +489,12 @@ export default { .cursor-pointer { cursor: pointer; } + +.x-not-active .x-active-show { + display: none; +} + +.x-active .x-active-hide { + display: none; +} diff --git a/varfish/vueapp/src/helpers.js b/varfish/vueapp/src/helpers.js index 5a305a642..b3541dc1b 100644 --- a/varfish/vueapp/src/helpers.js +++ b/varfish/vueapp/src/helpers.js @@ -18,6 +18,26 @@ export function getSubmissionLabel (item) { } } +export function removeItemOnce (arr, value) { + const index = arr.indexOf(value) + if (index > -1) { + arr.splice(index, 1) + } + return arr +} + +export function removeItemAll (arr, value) { + let i = 0 + while (i < arr.length) { + if (arr[i] === value) { + arr.splice(i, 1) + } else { + ++i + } + } + return arr +} + /** * Check current form for valid and display message or execute callback. * @@ -37,3 +57,54 @@ export function validConfirmed (cb, message = 'Please fix the problems first.', return true } } + +export function isDiseaseTerm (termId) { + return termId.startsWith('OMIM:') || termId.startsWith('ORPHA:') +} + +export const HPO_INHERITANCE_MODE = Object.freeze(new Map([ + ['HP:0001452', 'Autosomal dominant contiguous gene syndrome'], + ['HP:0025352', 'Autosomal dominant germline de novo mutation'], + ['HP:0000006', 'Autosomal dominant inheritance'], + ['HP:0012275', 'Autosomal dominant inheritance with maternal imprinting'], + ['HP:0012274', 'Autosomal dominant inheritance with paternal imprinting'], + ['HP:0001444', 'Autosomal dominant somatic cell mutation'], + ['HP:0000007', 'Autosomal recessive inheritance'], + ['HP:0001466', 'Contiguous gene syndrome'], + ['HP:0010984', 'Digenic inheritance'], + ['HP:0003743', 'Genetic anticipation'], + ['HP:0003744', 'Genetic anticipation with paternal anticipation bias'], + ['HP:0010985', 'Gonosomal inheritance'], + ['HP:0001475', 'Male-limited autosomal dominant'], + ['HP:0001427', 'Mitochondrial inheritance'], + ['HP:0001426', 'Multifactorial inheritance'], + ['HP:0010983', 'Oligogenic inheritance'], + ['HP:0010982', 'Polygenic inheritance'], + ['HP:0032113', 'Semidominant mode of inheritance'], + ['HP:0001470', 'Sex-limited autosomal dominant'], + ['HP:0031362', 'Sex-limited autosomal recessive inheritance'], + ['HP:0001442', 'Somatic mosaicism'], + ['HP:0001428', 'Somatic mutation'], + ['HP:0003745', 'Sporadic'], + ['HP:0032382', 'Uniparental disomy'], + ['HP:0032383', 'Uniparental heterodisomy'], + ['HP:0032384', 'Uniparental isodisomy'], + ['HP:0001423', 'X-linked dominant inheritance'], + ['HP:0001417', 'X-linked inheritance'], + ['HP:0001419', 'X-linked recessive inheritance'] +])) +export const HPO_AGE_OF_ONSET = Object.freeze(new Map([ + ['HP:0030674', 'Antenatal'], + ['HP:0011460', 'Embryonal'], + ['HP:0011461', 'Fetal'], + ['HP:0410280', 'Pediatric'], + ['HP:0003593', 'Infantile'], + ['HP:0011405', 'Childhood'], + ['HP:0003621', 'Juvenile'], + ['HP:0003581', 'Adult'], + ['HP:0003623', 'Neonatal'], + ['HP:0011462', 'Young adult'], + ['HP:0003596', 'Middle age'], + ['HP:0003584', 'Late'], + ['HP:0003577', 'Congenital'] +])) diff --git a/varfish/vueapp/src/store/modules/clinvarExport.js b/varfish/vueapp/src/store/modules/clinvarExport.js index 529eec05d..2e5e2084d 100644 --- a/varfish/vueapp/src/store/modules/clinvarExport.js +++ b/varfish/vueapp/src/store/modules/clinvarExport.js @@ -1,6 +1,6 @@ import Vue from 'vue' import clinvarExport from '../../api/clinvarExport' -import { uuidv4 } from '@/helpers' +import { uuidv4, isDiseaseTerm, HPO_INHERITANCE_MODE } from '@/helpers' /** * Enum for the valid clinvar export application states. @@ -103,10 +103,17 @@ const actions = { * Changes will be committed through `wizardSave`. */ createNewSubmissionSet ({ state, commit }) { + const titles = Object.values(state.submissionSets).map(submissionSet => submissionSet.title) + let title = 'New Submission Set' + let i = 2 + while (titles.includes(title)) { + title = 'New Submission Set #' + i + i += 1 + } const submissionSet = { sodar_uuid: uuidv4(), date_modified: new Date().toLocaleString(), - title: 'New Submission', + title: title, state: 'draft', sort_order: Object.keys(state.submissionSets).length, submitter: null, @@ -117,6 +124,7 @@ const actions = { commit('ADD_SUBMISSION_SET', submissionSet) commit('SET_CURRENT_SUBMISSION_SET', submissionSet.sodar_uuid) + commit('SET_WIZARD_STATE', WizardState.submissionSet) commit('SET_APP_STATE', AppState.add) }, /** @@ -334,10 +342,12 @@ const actions = { commit('SET_APP_STATE', AppState.list) for (const submittingOrgUuid of state.currentSubmissionSet.submitting_orgs) { - await clinvarExport.deleteSubmittingOrg( - state.submittingOrgs[submittingOrgUuid], - state.appContext - ) + if (submittingOrgUuid in state.oldModel.submittingOrgs) { + await clinvarExport.deleteSubmittingOrg( + state.submittingOrgs[submittingOrgUuid], + state.appContext + ) + } commit('DELETE_SUBMITTING_ORG', submittingOrgUuid) } @@ -345,23 +355,29 @@ const actions = { for (const submissionUuid of submissionUuids) { const submissionInvidualUuids = Array.from(state.submissions[submissionUuid].submission_individuals) for (const submissionInvidualUuid of submissionInvidualUuids) { - await clinvarExport.deleteSubmissionIndividual( - state.submissionIndividuals[submissionInvidualUuid], + if (submissionInvidualUuid in state.oldModel.submissionIndividuals) { + await clinvarExport.deleteSubmissionIndividual( + state.submissionIndividuals[submissionInvidualUuid], + state.appContext + ) + } + commit('DELETE_SUBMISSION_INDIVIDUAL', submissionInvidualUuid) + } + if (submissionUuid in state.oldModel.submissions) { + await clinvarExport.deleteSubmission( + state.submissions[submissionUuid], state.appContext ) - commit('DELETE_SUBMISSION_INDIVIDUAL', submissionInvidualUuid) } - await clinvarExport.deleteSubmission( - state.submissions[submissionUuid], - state.appContext - ) commit('DELETE_SUBMISSION', submissionUuid) } - await clinvarExport.deleteSubmissionSet( - state.submissionSets[state.currentSubmissionSet.sodar_uuid], - state.appContext - ) + if (state.currentSubmissionSet.sodar_uuid in state.oldModel.submissionSets) { + await clinvarExport.deleteSubmissionSet( + state.submissionSets[state.currentSubmissionSet.sodar_uuid], + state.appContext + ) + } commit('DELETE_SUBMISSION_SET', state.currentSubmissionSet.sodar_uuid) commit('SET_CURRENT_SUBMISSION_SET', null) @@ -510,21 +526,49 @@ function sodarObjectListToObject (lst) { * Extract variant zygosity information from state for the given smallVariant. */ function extractVariantZygosity (smallVariant, individualUuids, state) { - let variantAlleleCount = 0 + function getVariantZygosity (variantAlleleCount, isRecessive) { + if (variantAlleleCount === 2) { + return 'Homozygote' + } else { + if (isRecessive) { + return 'Compound heterozygote' + } else { + return 'Single heterozygote' + } + } + } + + // See whether any individual is annotated as recessive. + let anyRecessive = false + let variantAlleleCount = null let variantZygosity = null if (smallVariant !== null) { let individual = null for (const individualUuid of individualUuids) { - individual = state.individuals[individualUuid] - if (individual.name in smallVariant.genotype) { + const currIndividual = state.individuals[individualUuid] + if (individual === null) { + individual = currIndividual + } + console.log('A') + if (variantAlleleCount === null && (individual.name in smallVariant.genotype)) { variantAlleleCount = ((smallVariant.genotype[individual.name].gt || '').match(/1/g) || []).length - break } + console.log('B', individual) + if (individual !== null) { + // eslint-disable-next-line camelcase + for (const { term_id } of (individual.phenotype_terms || [])) { + anyRecessive = anyRecessive || (HPO_INHERITANCE_MODE.get(term_id) || '').includes('recessive') + } + } + console.log('C') + } + if (variantAlleleCount === null) { + variantAlleleCount = 0 } if (individual && variantAlleleCount) { if (smallVariant.chromosome.includes('X')) { if (individual.sex === 'female') { - variantZygosity = (variantAlleleCount === 2) ? 'Homozygote' : 'Single heterozygote' + variantZygosity = getVariantZygosity(variantAlleleCount, anyRecessive) } else { variantAlleleCount = 1 variantZygosity = 'Hemizygote' @@ -538,7 +582,7 @@ function extractVariantZygosity (smallVariant, individualUuids, state) { variantZygosity = 'Hemizygote' } } else { - variantZygosity = (variantAlleleCount === 2) ? 'Homozygote' : 'Single heterozygote' + variantZygosity = getVariantZygosity(variantAlleleCount, anyRecessive) } } } @@ -867,8 +911,8 @@ const mutations = { phenotypes: JSON.parse(JSON.stringify(state.individuals[individual.sodar_uuid].phenotype_terms)), variant_zygosity: variantZygosity, variant_allele_count: variantAlleleCount, - variant_origin: 'not provided', - source: 'not provided', + variant_origin: 'germline', + source: 'clinical testing', tissue: 'Blood', citations: [] } @@ -881,7 +925,6 @@ const mutations = { // when sent to the UUID so it must be then updated locally. const newSubmission = { ...submission, - _isInvalid: false, sodar_uuid: uuidv4(), sort_order: Object.keys(state.submissions).length, @@ -891,15 +934,17 @@ const mutations = { for (const individualUuid of individualUuids) { const individual = state.individuals[individualUuid] + const phenotypes = JSON.parse(JSON.stringify(individual.phenotype_terms || [])) + .filter(term => !HPO_INHERITANCE_MODE.has(term.term_id) && !isDiseaseTerm(term.term_id)) const newSubmissionIndividual = { sodar_uuid: uuidv4(), individual: individualUuid, submission: newSubmission.sodar_uuid, - phenotypes: JSON.parse(JSON.stringify(individual.phenotype_terms || [])), + phenotypes: phenotypes, variant_allele_count: variantAlleleCount, variant_zygosity: variantZygosity, - variant_origin: 'not provided', - source: 'not provided', + variant_origin: 'germline', + source: 'clinical testing', tissue: 'Blood', citations: [] }