@apollo-annotation/jbrowse-plugin-apollo 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.esm.js +10932 -10932
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +10845 -10846
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +18619 -21342
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +7 -7
  12. package/src/ApolloInternetAccount/model.ts +81 -63
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +49 -18
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +3 -1
  18. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  19. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  20. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +39 -203
  21. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  22. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -102
  23. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +31 -230
  24. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  25. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  26. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  27. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +62 -386
  28. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +6 -0
  29. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +122 -70
  30. package/src/components/AddAssembly.tsx +33 -37
  31. package/src/components/AddFeature.tsx +21 -18
  32. package/src/components/AddRefSeqAliases.tsx +56 -42
  33. package/src/components/CopyFeature.tsx +1 -1
  34. package/src/components/CreateApolloAnnotation.tsx +22 -10
  35. package/src/components/DeleteAssembly.tsx +2 -9
  36. package/src/components/DownloadGFF3.tsx +2 -2
  37. package/src/components/ManageChecks.tsx +2 -9
  38. package/src/components/ManageUsers.tsx +23 -22
  39. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  40. package/src/components/ViewChangeLog.tsx +25 -50
  41. package/src/components/ViewCheckResults.tsx +1 -7
  42. package/src/config.ts +3 -3
  43. package/src/index.ts +17 -16
  44. package/src/makeDisplayComponent.tsx +9 -13
  45. package/src/session/ClientDataStore.ts +32 -14
  46. package/src/session/session.ts +19 -27
  47. package/src/util/glyphUtils.ts +178 -1
  48. package/src/util/loadAssemblyIntoClient.ts +3 -2
@@ -2,13 +2,16 @@ import {
2
2
  type AnnotationFeature,
3
3
  type TranscriptPartCoding,
4
4
  } from '@apollo-annotation/mst'
5
+ import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
5
6
  import { type MenuItem } from '@jbrowse/core/ui'
6
7
  import {
7
8
  type AbstractSessionModel,
9
+ getContainingView,
8
10
  getFrame,
9
11
  intersection2,
10
12
  measureText,
11
13
  } from '@jbrowse/core/util'
14
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
12
15
  import { alpha } from '@mui/material'
13
16
  import equal from 'fast-deep-equal/es6'
14
17
  import { getSnapshot } from 'mobx-state-tree'
@@ -18,12 +21,19 @@ import { FilterTranscripts } from '../../components/FilterTranscripts'
18
21
  import {
19
22
  type MousePosition,
20
23
  type MousePositionWithFeature,
24
+ getAdjacentExons,
21
25
  getContextMenuItemsForFeature,
22
26
  getMinAndMaxPx,
23
27
  getOverlappingEdge,
24
28
  getRelatedFeatures,
29
+ getStreamIcon,
30
+ isCDSFeature,
31
+ isExonFeature,
25
32
  isMousePositionWithFeature,
33
+ // isTranscriptFeature,
26
34
  isSelectedFeature,
35
+ navToFeatureCenter,
36
+ selectFeatureAndOpenWidget,
27
37
  } from '../../util'
28
38
  import { type LinearApolloSixFrameDisplay } from '../stateModel'
29
39
  import { type LinearApolloSixFrameDisplayMouseEvents } from '../stateModel/mouseEvents'
@@ -109,14 +119,14 @@ function drawTextLabels(
109
119
  for (let i = labelArray.length - 1; i >= 0; --i) {
110
120
  const label = labelArray[i]
111
121
  ctx.fillStyle = label.color
112
- const labelRowX = Math.max(label.x + 1, 0)
122
+ const labelRowX = label.x + 1
113
123
  const labelRowY = label.y + label.h
114
124
  const textWidth = measureText(label.text, 10)
115
125
  if (label.isSelected) {
116
- ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
117
126
  ctx.font = 'bold '.concat(font)
118
127
  }
119
128
  if (label.text) {
129
+ ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
120
130
  ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth)
121
131
  ctx.font = font
122
132
  }
@@ -341,6 +351,30 @@ function draw(
341
351
  (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
342
352
  cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
343
353
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
354
+
355
+ // Draw lines to connect CDS features with shared mRNA parent
356
+ if (counter > 1) {
357
+ // Mid-point for intron line "hat"
358
+ const midPoint: [number, number] = [
359
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
360
+ Math.max(
361
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
362
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
363
+ ),
364
+ ]
365
+ ctx.strokeStyle = 'rgb(0, 128, 128)'
366
+ ctx.beginPath()
367
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
368
+ ctx.lineTo(...midPoint)
369
+ ctx.stroke()
370
+ ctx.moveTo(...midPoint)
371
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
372
+ ctx.stroke()
373
+ }
374
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
375
+ prevCDSTop = cdsTop + rowHeight / 2
376
+ counter += 1
377
+
344
378
  if (cdsWidthPx > 2) {
345
379
  ctx.clearRect(
346
380
  cdsStartPx + 1,
@@ -365,31 +399,6 @@ function draw(
365
399
  cdsHeight - 2,
366
400
  )
367
401
 
368
- // Draw lines to connect CDS features with shared mRNA parent
369
- if (counter > 1) {
370
- // Mid-point for intron line "hat"
371
- const midPoint: [number, number] = [
372
- (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
373
- Math.max(
374
- frame < 0
375
- ? rowHeight * featureLabelSpacer * highestRow + 1
376
- : 1, // Avoid render ceiling
377
- Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
378
- ),
379
- ]
380
- ctx.strokeStyle = 'rgb(0, 128, 128)'
381
- ctx.beginPath()
382
- ctx.moveTo(prevCDSEndPx, prevCDSTop)
383
- ctx.lineTo(...midPoint)
384
- ctx.stroke()
385
- ctx.moveTo(...midPoint)
386
- ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
387
- ctx.stroke()
388
- }
389
- prevCDSEndPx = cdsStartPx + cdsWidthPx
390
- prevCDSTop = cdsTop + rowHeight / 2
391
- counter += 1
392
-
393
402
  if (topFill && bottomFill) {
394
403
  ctx.fillStyle = topFill
395
404
  ctx.fillRect(
@@ -508,43 +517,42 @@ function drawHover(
508
517
  let counter = 1
509
518
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
510
519
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
520
+ const minX =
521
+ (lgv.bpToPx({
522
+ refName,
523
+ coord: cds.min,
524
+ regionNumber: layoutIndex,
525
+ })?.offsetPx ?? 0) - offsetPx
526
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
527
+ const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
528
+ const frameAdjust =
529
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
530
+ const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
531
+ if (counter > 1) {
532
+ // Mid-point for intron line "hat"
533
+ const midPoint: [number, number] = [
534
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
535
+ Math.max(
536
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
537
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
538
+ ),
539
+ ]
540
+ ctx.strokeStyle = 'rgb(0, 0, 0)'
541
+ ctx.lineWidth = 2
542
+ ctx.beginPath()
543
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
544
+ ctx.lineTo(...midPoint)
545
+ ctx.stroke()
546
+ ctx.moveTo(...midPoint)
547
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
548
+ ctx.stroke()
549
+ }
550
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
551
+ prevCDSTop = cdsTop + rowHeight / 2
552
+ counter += 1
511
553
  if (cdsWidthPx > 2) {
512
- const minX =
513
- (lgv.bpToPx({
514
- refName,
515
- coord: cds.min,
516
- regionNumber: layoutIndex,
517
- })?.offsetPx ?? 0) - offsetPx
518
- const cdsStartPx = reversed ? minX - cdsWidthPx : minX
519
- const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
520
- const frameAdjust =
521
- (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
522
- const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
523
554
  ctx.fillStyle = 'rgba(255,0,0,0.6)'
524
555
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
525
-
526
- if (counter > 1) {
527
- // Mid-point for intron line "hat"
528
- const midPoint: [number, number] = [
529
- (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
530
- Math.max(
531
- frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
532
- Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
533
- ),
534
- ]
535
- ctx.strokeStyle = 'rgb(0, 0, 0)'
536
- ctx.lineWidth = 2
537
- ctx.beginPath()
538
- ctx.moveTo(prevCDSEndPx, prevCDSTop)
539
- ctx.lineTo(...midPoint)
540
- ctx.stroke()
541
- ctx.moveTo(...midPoint)
542
- ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
543
- ctx.stroke()
544
- }
545
- prevCDSEndPx = cdsStartPx + cdsWidthPx
546
- prevCDSTop = cdsTop + rowHeight / 2
547
- counter += 1
548
556
  }
549
557
  }
550
558
  }
@@ -848,8 +856,13 @@ function getContextMenuItems(
848
856
  }
849
857
  if (isMousePositionWithFeature(mousePosition)) {
850
858
  const { bp, feature } = mousePosition
851
- for (const relatedFeature of getRelatedFeatures(feature, bp)) {
852
- const featureID: string | undefined = relatedFeature.attributes
859
+ let featuresUnderClick = getRelatedFeatures(feature, bp)
860
+ if (isCDSFeature(feature, session)) {
861
+ featuresUnderClick = getRelatedFeatures(feature, bp, true)
862
+ }
863
+
864
+ for (const feature of featuresUnderClick) {
865
+ const featureID: string | undefined = feature.attributes
853
866
  .get('gff_id')
854
867
  ?.toString()
855
868
  if (featureID && filteredTranscripts.includes(featureID)) {
@@ -857,9 +870,48 @@ function getContextMenuItems(
857
870
  }
858
871
  const contextMenuItemsForFeature = getContextMenuItemsForFeature(
859
872
  display,
860
- relatedFeature,
873
+ feature,
861
874
  )
862
- if (featureTypeOntology.isTypeOf(relatedFeature.type, 'exon')) {
875
+ if (isExonFeature(feature, session)) {
876
+ const adjacentExons = getAdjacentExons(
877
+ feature,
878
+ display,
879
+ mousePosition,
880
+ session,
881
+ )
882
+ const lgv = getContainingView(
883
+ display as BaseDisplayModel,
884
+ ) as unknown as LinearGenomeViewModel
885
+ if (adjacentExons.upstream) {
886
+ const exon = adjacentExons.upstream
887
+ contextMenuItemsForFeature.push({
888
+ label: 'Go to upstream exon',
889
+ icon: getStreamIcon(
890
+ feature.strand,
891
+ true,
892
+ lgv.displayedRegions.at(0)?.reversed,
893
+ ),
894
+ onClick: () => {
895
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
896
+ selectFeatureAndOpenWidget(display, exon)
897
+ },
898
+ })
899
+ }
900
+ if (adjacentExons.downstream) {
901
+ const exon = adjacentExons.downstream
902
+ contextMenuItemsForFeature.push({
903
+ label: 'Go to downstream exon',
904
+ icon: getStreamIcon(
905
+ feature.strand,
906
+ false,
907
+ lgv.displayedRegions.at(0)?.reversed,
908
+ ),
909
+ onClick: () => {
910
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
911
+ selectFeatureAndOpenWidget(display, exon)
912
+ },
913
+ })
914
+ }
863
915
  contextMenuItemsForFeature.push(
864
916
  {
865
917
  label: 'Merge exons',
@@ -874,7 +926,7 @@ function getContextMenuItems(
874
926
  doneCallback()
875
927
  },
876
928
  changeManager,
877
- sourceFeature: relatedFeature,
929
+ sourceFeature: feature,
878
930
  sourceAssemblyId: currentAssemblyId,
879
931
  selectedFeature,
880
932
  setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -898,7 +950,7 @@ function getContextMenuItems(
898
950
  doneCallback()
899
951
  },
900
952
  changeManager,
901
- sourceFeature: relatedFeature,
953
+ sourceFeature: feature,
902
954
  sourceAssemblyId: currentAssemblyId,
903
955
  selectedFeature,
904
956
  setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -911,7 +963,7 @@ function getContextMenuItems(
911
963
  },
912
964
  )
913
965
  }
914
- if (featureTypeOntology.isTypeOf(relatedFeature.type, 'gene')) {
966
+ if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
915
967
  contextMenuItemsForFeature.push({
916
968
  label: 'Filter alternate transcripts',
917
969
  onClick: () => {
@@ -922,7 +974,7 @@ function getContextMenuItems(
922
974
  handleClose: () => {
923
975
  doneCallback()
924
976
  },
925
- sourceFeature: relatedFeature,
977
+ sourceFeature: feature,
926
978
  filteredTranscripts: getSnapshot(filteredTranscripts),
927
979
  onUpdate: (forms: string[]) => {
928
980
  display.updateFilteredTranscripts(forms)
@@ -934,7 +986,7 @@ function getContextMenuItems(
934
986
  })
935
987
  }
936
988
  menuItems.push({
937
- label: relatedFeature.type,
989
+ label: feature.type,
938
990
  subMenu: contextMenuItemsForFeature,
939
991
  })
940
992
  }
@@ -39,7 +39,7 @@ import {
39
39
  } from '@mui/material'
40
40
  import ObjectID from 'bson-objectid'
41
41
  import { getRoot } from 'mobx-state-tree'
42
- import React, { useEffect, useState } from 'react'
42
+ import React, { useState } from 'react'
43
43
  import { makeStyles } from 'tss-react/mui'
44
44
 
45
45
  import { type ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
@@ -149,25 +149,8 @@ export function AddAssembly({
149
149
  const [fastaGziIndexUrl, setFastaGziIndexUrl] = useState<string>('')
150
150
 
151
151
  const [loading, setLoading] = useState(false)
152
- const [isGzip, setIsGzip] = useState<boolean>(false)
153
-
154
- useEffect(() => {
155
- setFastaIndexUrl(fastaUrl ? `${fastaUrl}.fai` : '')
156
- }, [fastaUrl])
157
-
158
- useEffect(() => {
159
- setFastaGziIndexUrl(fastaUrl ? `${fastaUrl}.gzi` : '')
160
- }, [fastaUrl])
161
-
162
- useEffect(() => {
163
- if (sequenceIsEditable || fileType === FileType.GFF3) {
164
- setIsGzip(
165
- fastaFile?.name.toLocaleLowerCase().endsWith('.gz') ? true : false,
166
- )
167
- } else {
168
- setIsGzip(true)
169
- }
170
- }, [fastaFile, sequenceIsEditable, fileType])
152
+ const [fastaGzipChecked, setFastaGzipChecked] = useState<boolean>(false)
153
+ const [gff3GzipChecked, setGff3GzipChecked] = useState<boolean>(false)
171
154
 
172
155
  function checkAssemblyName(assembly: string) {
173
156
  const { assemblies } = session as unknown as AbstractSessionModel
@@ -194,10 +177,13 @@ export function AddAssembly({
194
177
  const uri = url.href
195
178
  const formData = new FormData()
196
179
  let filename = file.name
180
+ const isGzip =
181
+ fileType === FileType.BGZIP_FASTA ||
182
+ (fileType === FileType.FASTA &&
183
+ (!sequenceIsEditable || fastaGzipChecked)) ||
184
+ (fileType === FileType.GFF3 && gff3GzipChecked)
197
185
 
198
- if (fileType === FileType.FAI || fileType === FileType.GZI) {
199
- filename = `${filename}.txt`
200
- } else if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
186
+ if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
201
187
  filename = `${filename}.gz`
202
188
  } else if (!isGzip && file.name.toLocaleLowerCase().endsWith('.gz')) {
203
189
  filename = `${filename}.txt`
@@ -370,11 +356,6 @@ export function AddAssembly({
370
356
  if (newExpanded) {
371
357
  setExpanded(panel)
372
358
  }
373
- if (panel === 'panelGffInput') {
374
- setIsGzip(false)
375
- } else {
376
- setIsGzip(true)
377
- }
378
359
  }
379
360
 
380
361
  return (
@@ -497,12 +478,12 @@ export function AddAssembly({
497
478
  data-testid="fasta-is-gzip-checkbox"
498
479
  control={
499
480
  <Checkbox
500
- checked={isGzip}
481
+ checked={!sequenceIsEditable || fastaGzipChecked}
501
482
  onChange={() => {
502
483
  if (sequenceIsEditable) {
503
- setIsGzip(!isGzip)
484
+ setFastaGzipChecked(!fastaGzipChecked)
504
485
  } else {
505
- setIsGzip(true)
486
+ setFastaGzipChecked(true)
506
487
  }
507
488
  }}
508
489
  disabled={!sequenceIsEditable}
@@ -533,7 +514,13 @@ export function AddAssembly({
533
514
  onChange={(
534
515
  e: React.ChangeEvent<HTMLInputElement>,
535
516
  ) => {
536
- setFastaFile(e.target.files?.item(0) ?? null)
517
+ const file = e.target.files?.item(0)
518
+ if (file) {
519
+ setFastaFile(file)
520
+ if (file.name.endsWith('.gz')) {
521
+ setFastaGzipChecked(true)
522
+ }
523
+ }
537
524
  }}
538
525
  disabled={submitted && !errorMessage}
539
526
  />
@@ -606,7 +593,10 @@ export function AddAssembly({
606
593
  onChange={(
607
594
  e: React.ChangeEvent<HTMLInputElement>,
608
595
  ) => {
609
- setFastaUrl(e.target.value)
596
+ const { value } = e.target
597
+ setFastaUrl(value)
598
+ setFastaIndexUrl(value ? `${value}.fai` : '')
599
+ setFastaGziIndexUrl(value ? `${value}.gzi` : '')
610
600
  }}
611
601
  disabled={submitted && !errorMessage}
612
602
  slotProps={{
@@ -727,8 +717,14 @@ export function AddAssembly({
727
717
  type="file"
728
718
  disabled={submitted && !errorMessage}
729
719
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
730
- setFastaFile(e.target.files?.item(0) ?? null)
731
- setFileType(FileType.GFF3)
720
+ const file = e.target.files?.item(0)
721
+ if (file) {
722
+ setFastaFile(file)
723
+ setFileType(FileType.GFF3)
724
+ if (file.name.endsWith('.gz')) {
725
+ setGff3GzipChecked(true)
726
+ }
727
+ }
732
728
  }}
733
729
  />
734
730
  <FormGroup style={{ display: 'grid' }}>
@@ -748,9 +744,9 @@ export function AddAssembly({
748
744
  data-testid="gff3-is-gzip-checkbox"
749
745
  control={
750
746
  <Checkbox
751
- checked={isGzip}
747
+ checked={gff3GzipChecked}
752
748
  onChange={() => {
753
- setIsGzip(!isGzip)
749
+ setGff3GzipChecked(!gff3GzipChecked)
754
750
  }}
755
751
  disabled={submitted && !errorMessage}
756
752
  />
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable @typescript-eslint/unbound-method */
2
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
3
2
 
4
3
  import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
5
4
  import { AddFeatureChange } from '@apollo-annotation/shared'
@@ -26,6 +25,7 @@ import {
26
25
  import ObjectID from 'bson-objectid'
27
26
  import React, { useState } from 'react'
28
27
 
28
+ import { CollaborationServerDriver } from '../BackendDrivers'
29
29
  import { type ChangeManager } from '../ChangeManager'
30
30
  import { isOntologyClass } from '../OntologyManager'
31
31
  import { type ApolloSessionModel } from '../session'
@@ -96,30 +96,32 @@ export function AddFeature({
96
96
  const [end, setEnd] = useState(String(region.end))
97
97
  const [start, setStart] = useState(String(region.start + 1))
98
98
  const [type, setType] = useState<NewFeature>(NewFeature.GENE_AND_SUBFEATURES)
99
- const [customType, setCustomType] = useState<string>()
99
+ const [customType, setCustomType] = useState<string>('')
100
100
  const [strand, setStrand] = useState<1 | -1 | undefined>()
101
101
  const [errorMessage, setErrorMessage] = useState('')
102
102
 
103
- function onSubmit(event: React.FormEvent<HTMLFormElement>) {
103
+ async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
104
104
  event.preventDefault()
105
105
  setErrorMessage('')
106
106
 
107
- let refSeqId
108
- for (const [, asm] of session.apolloDataStore.assemblies ?? new Map()) {
109
- if (asm._id === region.assemblyName) {
110
- for (const [, refseq] of asm.refSeqs ?? new Map()) {
111
- if (refseq.name === region.refName) {
112
- refSeqId = refseq._id
113
- }
114
- }
115
- }
107
+ const backendDriver = session.apolloDataStore.getBackendDriver(
108
+ region.assemblyName,
109
+ )
110
+ if (!backendDriver) {
111
+ setErrorMessage('No backend driver found')
112
+ return
116
113
  }
117
-
118
- if (!refSeqId) {
119
- setErrorMessage(
120
- 'Invalid refseq id. Make sure you have the Apollo annotation track open',
114
+ let refSeqId = region.refName
115
+ if (backendDriver instanceof CollaborationServerDriver) {
116
+ const backendRefSeqId = await backendDriver.getRefSeqId(
117
+ region.assemblyName,
118
+ region.refName,
121
119
  )
122
- return
120
+ if (!backendRefSeqId) {
121
+ setErrorMessage(`Could not find refSeq for "${region.refName}"`)
122
+ return
123
+ }
124
+ refSeqId = backendRefSeqId
123
125
  }
124
126
 
125
127
  if (type === NewFeature.GENE_AND_SUBFEATURES) {
@@ -248,6 +250,7 @@ export function AddFeature({
248
250
  maxWidth={false}
249
251
  data-testid="add-feature-dialog"
250
252
  >
253
+ {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
251
254
  <form onSubmit={onSubmit} data-testid="submit-form">
252
255
  <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
253
256
  <TextField
@@ -344,7 +347,7 @@ export function AddFeature({
344
347
  session={session}
345
348
  ontologyName="Sequence Ontology"
346
349
  style={{ width: 170 }}
347
- value=""
350
+ value={customType}
348
351
  filterTerms={isOntologyClass}
349
352
  renderInput={(params) => (
350
353
  <TextField
@@ -16,12 +16,19 @@ import {
16
16
  Select,
17
17
  type SelectChangeEvent,
18
18
  } from '@mui/material'
19
- import { DataGrid, type GridColDef, type GridRowModel } from '@mui/x-data-grid'
19
+ import {
20
+ DataGrid,
21
+ type GridColDef,
22
+ type GridRowModel,
23
+ type GridRowSelectionModel,
24
+ } from '@mui/x-data-grid'
25
+ import { observer } from 'mobx-react'
20
26
  import React, { useEffect, useRef, useState } from 'react'
21
27
 
22
28
  import {
23
29
  type ApolloInternetAccount,
24
30
  type CollaborationServerDriver,
31
+ type RefNameAliases,
25
32
  } from '../BackendDrivers'
26
33
  import { type ChangeManager } from '../ChangeManager'
27
34
  import { type ApolloSessionModel } from '../session'
@@ -30,7 +37,7 @@ import { Dialog } from './Dialog'
30
37
 
31
38
  const columns: GridColDef[] = [
32
39
  { field: 'refName', headerName: 'Ref Name' },
33
- { field: 'aliases', headerName: 'Aliases', editable: true },
40
+ { field: 'aliases', headerName: 'Aliases', editable: true, flex: 1 },
34
41
  ]
35
42
 
36
43
  interface AddChildFeatureProps {
@@ -44,7 +51,7 @@ const isGeneratedObjectId = (key: string): boolean => {
44
51
  return pattern.test(key)
45
52
  }
46
53
 
47
- export function AddRefSeqAliases({
54
+ export const AddRefSeqAliases = observer(function AddRefSeqAliases({
48
55
  changeManager,
49
56
  handleClose,
50
57
  session,
@@ -75,44 +82,50 @@ export function AddRefSeqAliases({
75
82
  const assemblies = collaborationServerDriver.getAssemblies()
76
83
 
77
84
  useEffect(() => {
78
- let retry = 0
79
- const maxRetries = 2
80
- const initializeRefNameAliasMap = () => {
81
- if (!selectedAssembly) {
82
- return
83
- }
84
- const initialMap = new Map<string, string[]>()
85
- if (retry < maxRetries && !selectedAssembly.refNames) {
86
- retry++
87
- setTimeout(initializeRefNameAliasMap, 50)
88
- }
89
- if (!selectedAssembly.refNames) {
90
- return
91
- }
92
- const refNameAliasess = selectedAssembly.refNameAliases
93
- for (const key in refNameAliasess) {
94
- const value = refNameAliasess[key]
95
- if (!value || isGeneratedObjectId(key)) {
96
- continue
97
- }
98
- if (initialMap.has(value)) {
99
- const aliases = initialMap.get(value) ?? []
100
- initialMap.set(value, [...aliases, key])
101
- } else {
102
- initialMap.set(value, [key])
103
- }
104
- }
105
- setRefNameAliasMap(initialMap)
85
+ if (assemblies.length > 0) {
86
+ setSelectedAssembly(assemblies[0])
87
+ collaborationServerDriver
88
+ .getRefNameAliases(assemblies[0].name)
89
+ .then((refNameAliases) => {
90
+ initializeRefNameAliasMap(refNameAliases)
91
+ })
92
+ .catch(() => {
93
+ setRefNameAliasMap(new Map())
94
+ setErrorMessage('Error fetching refName aliases for assembly')
95
+ })
106
96
  }
97
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
+ }, [])
107
99
 
108
- initializeRefNameAliasMap()
109
- }, [selectedAssembly])
100
+ const initializeRefNameAliasMap = (refNameAliasesList: RefNameAliases[]) => {
101
+ const initialMap = new Map<string, string[]>()
102
+ for (const refNameAliases of refNameAliasesList) {
103
+ const key = refNameAliases.refName
104
+ if (isGeneratedObjectId(key)) {
105
+ continue
106
+ }
107
+ initialMap.set(key, refNameAliases.aliases)
108
+ }
109
+ setRefNameAliasMap(initialMap)
110
+ }
110
111
 
111
112
  const handleChangeAssembly = (e: SelectChangeEvent) => {
112
113
  const newAssembly = assemblies.find((asm) => asm.name === e.target.value)
113
114
  setSelectedAssembly(newAssembly)
115
+ if (!newAssembly?.name) {
116
+ return
117
+ }
118
+ collaborationServerDriver
119
+ .getRefNameAliases(newAssembly.name)
120
+ .then((refNameAliases) => {
121
+ initializeRefNameAliasMap(refNameAliases)
122
+ setErrorMessage('')
123
+ })
124
+ .catch(() => {
125
+ setRefNameAliasMap(new Map())
126
+ setErrorMessage('Error fetching refName aliases for assembly')
127
+ })
114
128
  setEnableSubmit(false)
115
- setErrorMessage('')
116
129
  if (fileRef.current) {
117
130
  fileRef.current.value = ''
118
131
  }
@@ -145,11 +158,12 @@ export function AddRefSeqAliases({
145
158
  })
146
159
  }
147
160
 
148
- const rowSelectionChange = (ids: number[]) => {
149
- if (ids.length > 0) {
161
+ const rowSelectionChange = (gridRowSelectionModel: GridRowSelectionModel) => {
162
+ const { ids } = gridRowSelectionModel
163
+ if (ids.size > 0) {
150
164
  setEnableSubmit(true)
151
- const selectedRows = ids.flatMap((id) =>
152
- getTableRows().filter((row) => row.id === id),
165
+ const selectedRows = [...ids.values()].flatMap((id) =>
166
+ getTableRows().filter((row) => String(row.id) === String(id)),
153
167
  )
154
168
  setSelectedRows(selectedRows)
155
169
  } else {
@@ -222,6 +236,7 @@ export function AddRefSeqAliases({
222
236
  label="Assembly"
223
237
  value={selectedAssembly?.name ?? ''}
224
238
  onChange={handleChangeAssembly}
239
+ style={{ minWidth: 150 }}
225
240
  >
226
241
  {assemblies.map((option) => (
227
242
  <MenuItem key={option.name} value={option.name}>
@@ -255,11 +270,10 @@ export function AddRefSeqAliases({
255
270
  },
256
271
  }}
257
272
  pageSizeOptions={[5, 10]}
258
- onRowSelectionModelChange={(ids) => {
259
- rowSelectionChange(ids as unknown as number[])
260
- }}
273
+ onRowSelectionModelChange={rowSelectionChange}
261
274
  processRowUpdate={processRowUpdate}
262
275
  checkboxSelection
276
+ disableRowSelectionExcludeModel
263
277
  ></DataGrid>
264
278
  </div>
265
279
  ) : null}
@@ -284,4 +298,4 @@ export function AddRefSeqAliases({
284
298
  ) : null}
285
299
  </Dialog>
286
300
  )
287
- }
301
+ })
@@ -99,8 +99,8 @@ export function CopyFeature({
99
99
  }
100
100
 
101
101
  useEffect(() => {
102
- setSelectedRefSeqId('')
103
102
  async function getRefNames() {
103
+ setSelectedRefSeqId('')
104
104
  if (!selectedAssemblyId) {
105
105
  setErrorMessage('No assemblies to copy to')
106
106
  return