@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 0.3.7

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 (56) hide show
  1. package/dist/index.esm.js +2679 -850
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
  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 +5194 -1258
  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 +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
  13. package/src/ChangeManager.ts +10 -6
  14. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  15. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
  17. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
  23. package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
  26. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
  27. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  28. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
  29. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
  30. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
  31. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
  33. package/src/OntologyManager/index.ts +4 -1
  34. package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
  35. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  36. package/src/components/AddAssemblyAliases.tsx +114 -0
  37. package/src/components/AddChildFeature.tsx +3 -6
  38. package/src/components/AddFeature.tsx +14 -15
  39. package/src/components/CopyFeature.tsx +2 -4
  40. package/src/components/CreateApolloAnnotation.tsx +334 -151
  41. package/src/components/DeleteFeature.tsx +358 -11
  42. package/src/components/DownloadGFF3.tsx +20 -1
  43. package/src/components/FilterTranscripts.tsx +86 -0
  44. package/src/components/MergeExons.tsx +193 -0
  45. package/src/components/MergeTranscripts.tsx +185 -0
  46. package/src/components/SplitExon.tsx +134 -0
  47. package/src/components/index.ts +3 -0
  48. package/src/config.ts +5 -0
  49. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  50. package/src/extensions/annotationFromPileup.ts +99 -89
  51. package/src/session/session.ts +26 -13
  52. package/src/util/annotationFeatureUtils.ts +65 -0
  53. package/src/util/copyToClipboard.ts +21 -0
  54. package/src/util/glyphUtils.ts +49 -0
  55. package/src/util/index.ts +2 -0
  56. package/src/util/mouseEventsUtils.ts +113 -0
@@ -32,7 +32,9 @@ import {
32
32
  import { observer } from 'mobx-react'
33
33
  import React, { useRef } from 'react'
34
34
 
35
+ import { type OntologyRecord } from '../OntologyManager'
35
36
  import { type ApolloSessionModel } from '../session'
37
+ import { copyToClipboard } from '../util/copyToClipboard'
36
38
 
37
39
  import { StyledAccordionSummary } from './ApolloTranscriptDetailsWidget'
38
40
  import { NumberTextField } from './NumberTextField'
@@ -81,6 +83,19 @@ const Strand = (props: { strand: 1 | -1 | undefined }) => {
81
83
  )
82
84
  }
83
85
 
86
+ const minMaxExonTranscriptLocation = (
87
+ transcript: AnnotationFeature,
88
+ featureTypeOntology: OntologyRecord,
89
+ ) => {
90
+ const { transcriptExonParts } = transcript
91
+ const exonParts = transcriptExonParts
92
+ .filter((part) => featureTypeOntology.isTypeOf(part.type, 'exon'))
93
+ .sort(({ min: a }, { min: b }) => a - b)
94
+ const exonMin: number = exonParts[0]?.min
95
+ const exonMax: number = exonParts[exonParts.length - 1]?.max
96
+ return [exonMin, exonMax]
97
+ }
98
+
84
99
  export const TranscriptWidgetEditLocation = observer(
85
100
  function TranscriptWidgetEditLocation({
86
101
  assembly,
@@ -99,8 +114,41 @@ export const TranscriptWidgetEditLocation = observer(
99
114
  const { changeManager } = session.apolloDataStore
100
115
  const seqRef = useRef<HTMLDivElement>(null)
101
116
 
102
- // Separate function to handle CDS location change
103
- // because start of CDS and exon might be same
117
+ if (!refData) {
118
+ return null
119
+ }
120
+
121
+ const { apolloDataStore } = session
122
+ const { featureTypeOntology } =
123
+ apolloDataStore.ontologyManager as unknown as {
124
+ featureTypeOntology: OntologyRecord
125
+ }
126
+
127
+ if (
128
+ !featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
129
+ !featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
130
+ ) {
131
+ throw new Error('Feature is not a transcript or equivalent')
132
+ }
133
+
134
+ const { cdsLocations, transcriptExonParts, strand } = feature
135
+ const [firstCDSLocation] = cdsLocations
136
+ const [exonMin, exonMax] = minMaxExonTranscriptLocation(
137
+ feature,
138
+ featureTypeOntology,
139
+ )
140
+ let cdsMin = exonMin
141
+ let cdsMax = exonMax
142
+ const cdsPresent = firstCDSLocation.length > 0
143
+
144
+ if (cdsPresent) {
145
+ const sortedCDSLocations = firstCDSLocation.toSorted(
146
+ ({ min: a }, { min: b }) => a - b,
147
+ )
148
+ cdsMin = sortedCDSLocations[0].min
149
+ cdsMax = sortedCDSLocations[sortedCDSLocations.length - 1].max
150
+ }
151
+
104
152
  function handleCDSLocationChange(
105
153
  oldLocation: number,
106
154
  newLocation: number,
@@ -110,39 +158,245 @@ export const TranscriptWidgetEditLocation = observer(
110
158
  if (!feature.children) {
111
159
  throw new Error('Transcript should have child features')
112
160
  }
113
- for (const [, child] of feature.children) {
114
- if (child.type !== 'CDS') {
115
- continue
161
+
162
+ const overlappingExon = getOverlappingExonForCDS(
163
+ feature,
164
+ featureTypeOntology,
165
+ oldLocation,
166
+ isMin,
167
+ )
168
+ if (!overlappingExon) {
169
+ notify('No matching exon found', 'error')
170
+ return
171
+ }
172
+ const oldExonLocation = isMin ? overlappingExon.min : overlappingExon.max
173
+ const { prevExon, nextExon } = getNeighboringExonParts(
174
+ feature,
175
+ featureTypeOntology,
176
+ oldExonLocation,
177
+ isMin,
178
+ )
179
+
180
+ // Start location should be less than end location
181
+ if (isMin && newLocation >= overlappingExon.max) {
182
+ notify(
183
+ 'Start location should be less than overlapping exon end location',
184
+ 'error',
185
+ )
186
+ return
187
+ }
188
+
189
+ // End location should be greater than start location
190
+ if (!isMin && newLocation <= overlappingExon.min) {
191
+ notify(
192
+ 'End location should be greater than overlapping exon start location',
193
+ 'error',
194
+ )
195
+ return
196
+ }
197
+ // Changed location should be greater than end location of previous exon - give 2bp buffer
198
+ if (prevExon && prevExon.max + 2 > newLocation) {
199
+ notify(
200
+ 'Start location should be greater than previous exon end location',
201
+ 'error',
202
+ )
203
+ return
204
+ }
205
+ // Changed location should be less than start location of next exon
206
+ if (nextExon && nextExon.min - 2 < newLocation) {
207
+ notify(
208
+ 'End location should be less than next exon start location',
209
+ 'error',
210
+ )
211
+ return
212
+ }
213
+
214
+ const cdsFeature = getMatchingCDSFeature(
215
+ feature,
216
+ featureTypeOntology,
217
+ oldLocation,
218
+ isMin,
219
+ )
220
+
221
+ if (!cdsFeature) {
222
+ notify('No matching CDS feature found', 'error')
223
+ return
224
+ }
225
+
226
+ if (!isMin && newLocation <= cdsFeature.min) {
227
+ notify(
228
+ 'End location should be greater than CDS start location',
229
+ 'error',
230
+ )
231
+ return
232
+ }
233
+ if (isMin && newLocation >= cdsFeature.max) {
234
+ notify('Start location should be less than CDS end location', 'error')
235
+ return
236
+ }
237
+
238
+ const overlappingExonFeature = getExonFeature(
239
+ feature,
240
+ overlappingExon.min,
241
+ overlappingExon.max,
242
+ featureTypeOntology,
243
+ )
244
+
245
+ if (!overlappingExonFeature) {
246
+ notify('No matching exon feature found', 'error')
247
+ return
248
+ }
249
+
250
+ if (isMin && newLocation !== cdsFeature.min) {
251
+ const startChange: LocationStartChange = new LocationStartChange({
252
+ typeName: 'LocationStartChange',
253
+ changedIds: [],
254
+ changes: [],
255
+ assembly,
256
+ })
257
+
258
+ if (newLocation < overlappingExon.min) {
259
+ if (prevExon) {
260
+ // update exon start location
261
+ appendStartLocationChange(
262
+ overlappingExonFeature,
263
+ startChange,
264
+ newLocation,
265
+ )
266
+ // update CDS start location
267
+ appendStartLocationChange(cdsFeature, startChange, newLocation)
268
+ } else {
269
+ const transcriptStart = feature.min
270
+ const gene = feature.parent
271
+ if (newLocation < transcriptStart) {
272
+ if (gene && newLocation < gene.min) {
273
+ // update gene start location
274
+ appendStartLocationChange(gene, startChange, newLocation)
275
+ }
276
+ // update transcript start location
277
+ appendStartLocationChange(feature, startChange, newLocation)
278
+ // update exon start location
279
+ appendStartLocationChange(
280
+ overlappingExonFeature,
281
+ startChange,
282
+ newLocation,
283
+ )
284
+ // update CDS start location
285
+ appendStartLocationChange(cdsFeature, startChange, newLocation)
286
+ }
287
+ }
288
+ } else {
289
+ // update CDS start location
290
+ appendStartLocationChange(cdsFeature, startChange, newLocation)
291
+ }
292
+
293
+ void changeManager.submit(startChange).catch(() => {
294
+ notify('Error updating feature CDS start position', 'error')
295
+ })
296
+ }
297
+
298
+ if (!isMin && newLocation !== cdsFeature.max) {
299
+ const endChange: LocationEndChange = new LocationEndChange({
300
+ typeName: 'LocationEndChange',
301
+ changedIds: [],
302
+ changes: [],
303
+ assembly,
304
+ })
305
+
306
+ if (newLocation > overlappingExon.max) {
307
+ if (nextExon) {
308
+ // update exon end location
309
+ appendEndLocationChange(
310
+ overlappingExonFeature,
311
+ endChange,
312
+ newLocation,
313
+ )
314
+ // update CDS end location
315
+ appendEndLocationChange(cdsFeature, endChange, newLocation)
316
+ } else {
317
+ const transcriptEnd = feature.max
318
+ const gene = feature.parent
319
+ if (newLocation > transcriptEnd) {
320
+ if (gene && newLocation > gene.max) {
321
+ // update gene end location
322
+ appendEndLocationChange(gene, endChange, newLocation)
323
+ }
324
+ // update transcript end location
325
+ appendEndLocationChange(feature, endChange, newLocation)
326
+ // update exon end location
327
+ appendEndLocationChange(
328
+ overlappingExonFeature,
329
+ endChange,
330
+ newLocation,
331
+ )
332
+ // update CDS end location
333
+ appendEndLocationChange(cdsFeature, endChange, newLocation)
334
+ }
335
+ }
336
+ } else {
337
+ // update CDS end location
338
+ appendEndLocationChange(cdsFeature, endChange, newLocation)
116
339
  }
117
- if (isMin && oldLocation === child.min) {
118
- const change = new LocationStartChange({
340
+
341
+ void changeManager.submit(endChange).catch(() => {
342
+ notify('Error updating feature CDS end position', 'error')
343
+ })
344
+ }
345
+ }
346
+
347
+ const updateCDSLocation = (
348
+ oldLocation: number,
349
+ newLocation: number,
350
+ feature: AnnotationFeature,
351
+ isMin: boolean,
352
+ onComplete?: () => void,
353
+ ) => {
354
+ if (!feature.children) {
355
+ throw new Error('Transcript should have child features')
356
+ }
357
+ if (oldLocation === newLocation) {
358
+ return
359
+ }
360
+
361
+ const cdsFeature = getMatchingCDSFeature(
362
+ feature,
363
+ featureTypeOntology,
364
+ oldLocation,
365
+ isMin,
366
+ )
367
+ if (!cdsFeature) {
368
+ notify('No matching CDS feature found', 'error')
369
+ return
370
+ }
371
+
372
+ const change = isMin
373
+ ? new LocationStartChange({
119
374
  typeName: 'LocationStartChange',
120
- changedIds: [child._id],
121
- featureId: feature._id,
122
- oldStart: child.min,
375
+ changedIds: [cdsFeature._id],
376
+ featureId: cdsFeature._id,
377
+ oldStart: cdsFeature.min,
123
378
  newStart: newLocation,
124
379
  assembly,
125
380
  })
126
- changeManager.submit(change).catch(() => {
127
- notify('Error updating feature start position', 'error')
128
- })
129
- return
130
- }
131
- if (!isMin && oldLocation === child.max) {
132
- const change = new LocationEndChange({
381
+ : new LocationEndChange({
133
382
  typeName: 'LocationEndChange',
134
- changedIds: [child._id],
135
- featureId: feature._id,
136
- oldEnd: child.max,
383
+ changedIds: [cdsFeature._id],
384
+ featureId: cdsFeature._id,
385
+ oldEnd: cdsFeature.max,
137
386
  newEnd: newLocation,
138
387
  assembly,
139
388
  })
140
- changeManager.submit(change).catch(() => {
141
- notify('Error updating feature start position', 'error')
142
- })
143
- return
144
- }
145
- }
389
+
390
+ void changeManager
391
+ .submit(change)
392
+ .then(() => {
393
+ if (onComplete) {
394
+ onComplete()
395
+ }
396
+ })
397
+ .catch(() => {
398
+ notify('Error updating feature CDS position', 'error')
399
+ })
146
400
  }
147
401
 
148
402
  function handleExonLocationChange(
@@ -154,62 +408,400 @@ export const TranscriptWidgetEditLocation = observer(
154
408
  if (!feature.children) {
155
409
  throw new Error('Transcript should have child features')
156
410
  }
157
- for (const [, child] of feature.children) {
158
- if (child.type !== 'exon') {
411
+ const { matchingExon, prevExon, nextExon } = getNeighboringExonParts(
412
+ feature,
413
+ featureTypeOntology,
414
+ oldLocation,
415
+ isMin,
416
+ )
417
+
418
+ if (!matchingExon) {
419
+ notify('No matching exon found', 'error')
420
+ return
421
+ }
422
+
423
+ // Start location should be less than end location
424
+ if (isMin && newLocation >= matchingExon.max) {
425
+ notify(`Start location should be less than end location`, 'error')
426
+ return
427
+ }
428
+ // End location should be greater than start location
429
+ if (!isMin && newLocation <= matchingExon.min) {
430
+ notify(`End location should be greater than start location`, 'error')
431
+ return
432
+ }
433
+ // Changed location should be greater than end location of previous exon - give 2bp buffer
434
+ if (prevExon && prevExon.max + 2 > newLocation) {
435
+ notify(`Error while changing start location`, 'error')
436
+ return
437
+ }
438
+ // Changed location should be less than start location of next exon - give 2bp buffer
439
+ if (nextExon && nextExon.min - 2 < newLocation) {
440
+ notify(`Error while changing end location`, 'error')
441
+ return
442
+ }
443
+
444
+ const exonFeature = getExonFeature(
445
+ feature,
446
+ matchingExon.min,
447
+ matchingExon.max,
448
+ featureTypeOntology,
449
+ )
450
+ if (!exonFeature) {
451
+ notify('No matching exon feature found', 'error')
452
+ return
453
+ }
454
+
455
+ const cdsFeature = getFirstCDSFeature(feature, featureTypeOntology)
456
+
457
+ // START LOCATION CHANGE
458
+ if (isMin && newLocation !== matchingExon.min) {
459
+ const startChange = new LocationStartChange({
460
+ typeName: 'LocationStartChange',
461
+ changedIds: [],
462
+ changes: [],
463
+ assembly,
464
+ })
465
+ if (prevExon) {
466
+ // update exon start location
467
+ appendStartLocationChange(exonFeature, startChange, newLocation)
468
+ } else {
469
+ const transcriptStart = feature.min
470
+ const gene = feature.parent
471
+ if (newLocation < transcriptStart) {
472
+ if (gene && newLocation < gene.min) {
473
+ // update gene start location
474
+ appendStartLocationChange(gene, startChange, newLocation)
475
+ }
476
+ // update transcript start location
477
+ appendStartLocationChange(feature, startChange, newLocation)
478
+ // update exon start location
479
+ appendStartLocationChange(exonFeature, startChange, newLocation)
480
+ } else if (newLocation > transcriptStart) {
481
+ // update exon start location
482
+ appendStartLocationChange(exonFeature, startChange, newLocation)
483
+ // update transcript start location
484
+ appendStartLocationChange(feature, startChange, newLocation)
485
+
486
+ if (gene) {
487
+ const [geneMinWithNewLoc] = geneMinMaxWithNewLocation(
488
+ gene,
489
+ feature,
490
+ newLocation,
491
+ featureTypeOntology,
492
+ isMin,
493
+ )
494
+ if (gene.min != geneMinWithNewLoc) {
495
+ // update gene start location
496
+ appendStartLocationChange(gene, startChange, geneMinWithNewLoc)
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ // When we change the start location of the exon overlapping with start location of the CDS
503
+ // and the new start location is greater than the CDS start location then we need to update the CDS start location
504
+ if (
505
+ cdsFeature &&
506
+ cdsFeature.min >= matchingExon.min &&
507
+ cdsFeature.min <= matchingExon.max &&
508
+ newLocation > cdsFeature.min
509
+ ) {
510
+ // update CDS start location
511
+ appendStartLocationChange(cdsFeature, startChange, newLocation)
512
+ }
513
+
514
+ void changeManager.submit(startChange).catch(() => {
515
+ notify('Error updating feature exon start position', 'error')
516
+ })
517
+ }
518
+
519
+ // END LOCATION CHANGE
520
+ if (!isMin && newLocation !== matchingExon.max) {
521
+ const endChange = new LocationEndChange({
522
+ typeName: 'LocationEndChange',
523
+ changedIds: [],
524
+ changes: [],
525
+ assembly,
526
+ })
527
+ if (nextExon) {
528
+ // update exon end location
529
+ appendEndLocationChange(exonFeature, endChange, newLocation)
530
+ } else {
531
+ const transcriptEnd = feature.max
532
+ const gene = feature.parent
533
+ if (newLocation > transcriptEnd) {
534
+ if (gene && newLocation > gene.max) {
535
+ // update gene end location
536
+ appendEndLocationChange(gene, endChange, newLocation)
537
+ }
538
+ // update transcript end location
539
+ appendEndLocationChange(feature, endChange, newLocation)
540
+ // update exon end location
541
+ appendEndLocationChange(exonFeature, endChange, newLocation)
542
+ } else if (newLocation < transcriptEnd) {
543
+ // update exon end location
544
+ appendEndLocationChange(exonFeature, endChange, newLocation)
545
+ // update transcript end location
546
+ appendEndLocationChange(feature, endChange, newLocation)
547
+
548
+ if (gene) {
549
+ const [, geneMaxWithNewLoc] = geneMinMaxWithNewLocation(
550
+ gene,
551
+ feature,
552
+ newLocation,
553
+ featureTypeOntology,
554
+ isMin,
555
+ )
556
+ if (gene.max != geneMaxWithNewLoc) {
557
+ // update gene end location
558
+ appendEndLocationChange(gene, endChange, geneMaxWithNewLoc)
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ // When we change the end location of the exon overlapping with end location of the CDS
565
+ // and the new end location is less than the CDS end location then we need to update the CDS end location
566
+ if (
567
+ cdsFeature &&
568
+ cdsFeature.max >= matchingExon.min &&
569
+ cdsFeature.max <= matchingExon.max &&
570
+ newLocation < cdsFeature.max
571
+ ) {
572
+ // update CDS end location
573
+ appendEndLocationChange(cdsFeature, endChange, newLocation)
574
+ }
575
+
576
+ void changeManager.submit(endChange).catch(() => {
577
+ notify('Error updating feature exon end position', 'error')
578
+ })
579
+ }
580
+ }
581
+
582
+ const appendEndLocationChange = (
583
+ feature: AnnotationFeature,
584
+ change: LocationEndChange,
585
+ newLocation: number,
586
+ ) => {
587
+ change.changedIds.push(feature._id)
588
+ change.changes.push({
589
+ featureId: feature._id,
590
+ oldEnd: feature.max,
591
+ newEnd: newLocation,
592
+ })
593
+ }
594
+
595
+ const appendStartLocationChange = (
596
+ feature: AnnotationFeature,
597
+ change: LocationStartChange,
598
+ newLocation: number,
599
+ ) => {
600
+ change.changedIds.push(feature._id)
601
+ change.changes.push({
602
+ featureId: feature._id,
603
+ oldStart: feature.min,
604
+ newStart: newLocation,
605
+ })
606
+ }
607
+
608
+ const getMatchingCDSFeature = (
609
+ feature: AnnotationFeature,
610
+ featureTypeOntology: OntologyRecord,
611
+ oldCDSLocation: number,
612
+ isMin: boolean,
613
+ ) => {
614
+ let cdsFeature
615
+ for (const [, child] of feature.children ?? []) {
616
+ if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
159
617
  continue
160
618
  }
161
- if (isMin && oldLocation === child.min) {
162
- const change = new LocationStartChange({
163
- typeName: 'LocationStartChange',
164
- changedIds: [child._id],
165
- featureId: feature._id,
166
- oldStart: child.min,
167
- newStart: newLocation,
168
- assembly,
169
- })
170
- changeManager.submit(change).catch(() => {
171
- notify('Error updating feature start position', 'error')
172
- })
173
- return
619
+
620
+ if (isMin && oldCDSLocation === child.min) {
621
+ cdsFeature = child
622
+ break
174
623
  }
175
- if (!isMin && oldLocation === child.max) {
176
- const change = new LocationEndChange({
177
- typeName: 'LocationEndChange',
178
- changedIds: [child._id],
179
- featureId: feature._id,
180
- oldEnd: child.max,
181
- newEnd: newLocation,
182
- assembly,
183
- })
184
- changeManager.submit(change).catch(() => {
185
- notify('Error updating feature start position', 'error')
186
- })
187
- return
624
+ if (!isMin && oldCDSLocation === child.max) {
625
+ cdsFeature = child
626
+ break
188
627
  }
189
628
  }
629
+ return cdsFeature
190
630
  }
191
631
 
192
- if (!refData) {
193
- return null
632
+ const getFirstCDSFeature = (
633
+ feature: AnnotationFeature,
634
+ featureTypeOntology: OntologyRecord,
635
+ ) => {
636
+ let cdsFeature
637
+ for (const [, child] of feature.children ?? []) {
638
+ if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
639
+ continue
640
+ }
641
+ cdsFeature = child
642
+ break
643
+ }
644
+ return cdsFeature
194
645
  }
195
646
 
196
- const { cdsLocations, transcriptExonParts, strand } = feature
197
- const [firstCDSLocation] = cdsLocations
647
+ const getExonFeature = (
648
+ feature: AnnotationFeature,
649
+ exonMin: number,
650
+ exonMax: number,
651
+ featureTypeOntology: OntologyRecord,
652
+ ) => {
653
+ let exonFeature
654
+ for (const [, child] of feature.children ?? []) {
655
+ if (!featureTypeOntology.isTypeOf(child.type, 'exon')) {
656
+ continue
657
+ }
658
+ if (exonMin === child.min && exonMax === child.max) {
659
+ exonFeature = child
660
+ break
661
+ }
662
+ }
663
+ return exonFeature
664
+ }
198
665
 
199
- const exonParts = transcriptExonParts
200
- .filter((part) => part.type === 'exon')
201
- .sort(({ min: a }, { min: b }) => a - b)
666
+ const geneMinMaxWithNewLocation = (
667
+ gene: AnnotationFeature,
668
+ transcript: AnnotationFeature,
669
+ newLocation: number,
670
+ featureTypeOntology: OntologyRecord,
671
+ isMin: boolean,
672
+ ) => {
673
+ const mins = []
674
+ const maxs = []
675
+ for (const [, t] of gene.children?.entries() ?? []) {
676
+ if (!featureTypeOntology.isTypeOf(t.type, 'transcript')) {
677
+ continue
678
+ }
202
679
 
203
- const exonMin: number = exonParts[0]?.min
204
- const exonMax: number = exonParts[exonParts.length - 1]?.max
680
+ if (t._id === transcript._id) {
681
+ if (isMin) {
682
+ mins.push(newLocation)
683
+ maxs.push(t.max)
684
+ } else {
685
+ maxs.push(newLocation)
686
+ mins.push(t.min)
687
+ }
688
+ } else {
689
+ mins.push(t.min)
690
+ maxs.push(t.max)
691
+ }
692
+ }
205
693
 
206
- let cdsMin = exonMin
207
- let cdsMax = exonMax
208
- const cdsPresent = firstCDSLocation.length > 0
694
+ const newMin = Math.min(...mins)
695
+ const newMax = Math.max(...maxs)
696
+ return [newMin, newMax]
697
+ }
209
698
 
210
- if (cdsPresent) {
211
- cdsMin = firstCDSLocation[0].min
212
- cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max
699
+ const getOverlappingExonForCDS = (
700
+ transcript: AnnotationFeature,
701
+ featureTypeOntology: OntologyRecord,
702
+ oldCDSLocation: number,
703
+ isMin: boolean,
704
+ ) => {
705
+ const { transcriptExonParts } = transcript
706
+ let overlappingExonPart
707
+ for (const [, exonPart] of transcriptExonParts.entries()) {
708
+ if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
709
+ continue
710
+ }
711
+ if (
712
+ !isMin &&
713
+ oldCDSLocation >= exonPart.min &&
714
+ oldCDSLocation <= exonPart.max
715
+ ) {
716
+ overlappingExonPart = exonPart
717
+ break
718
+ }
719
+ if (
720
+ isMin &&
721
+ oldCDSLocation >= exonPart.min &&
722
+ oldCDSLocation <= exonPart.max
723
+ ) {
724
+ overlappingExonPart = exonPart
725
+ break
726
+ }
727
+ }
728
+ return overlappingExonPart
729
+ }
730
+
731
+ const getNeighboringExonParts = (
732
+ transcript: AnnotationFeature,
733
+ featureTypeOntology: OntologyRecord,
734
+ oldExonLoc: number,
735
+ isMin: boolean,
736
+ ) => {
737
+ const { transcriptExonParts, strand } = transcript
738
+ let matchingExon, matchingExonIdx, prevExon, nextExon
739
+ for (const [i, exonPart] of transcriptExonParts.entries()) {
740
+ if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
741
+ continue
742
+ }
743
+ if (isMin && exonPart.min === oldExonLoc) {
744
+ matchingExon = exonPart
745
+ matchingExonIdx = i
746
+ break
747
+ }
748
+ if (!isMin && exonPart.max === oldExonLoc) {
749
+ matchingExon = exonPart
750
+ matchingExonIdx = i
751
+ break
752
+ }
753
+ }
754
+
755
+ if (matchingExon && matchingExonIdx !== undefined) {
756
+ if (strand === 1 && matchingExonIdx > 0) {
757
+ for (let i = matchingExonIdx - 1; i >= 0; i--) {
758
+ const prevLoc = transcriptExonParts[i]
759
+ if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
760
+ prevExon = prevLoc
761
+ break
762
+ }
763
+ }
764
+ }
765
+
766
+ if (strand === -1 && matchingExonIdx < transcriptExonParts.length - 1) {
767
+ for (
768
+ let i = matchingExonIdx + 1;
769
+ i < transcriptExonParts.length;
770
+ i++
771
+ ) {
772
+ const prevLoc = transcriptExonParts[i]
773
+ if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
774
+ prevExon = prevLoc
775
+ break
776
+ }
777
+ }
778
+ }
779
+
780
+ if (strand === 1 && matchingExonIdx < transcriptExonParts.length - 1) {
781
+ for (
782
+ let i = matchingExonIdx + 1;
783
+ i < transcriptExonParts.length;
784
+ i++
785
+ ) {
786
+ const nextLoc = transcriptExonParts[i]
787
+ if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
788
+ nextExon = nextLoc
789
+ break
790
+ }
791
+ }
792
+ }
793
+
794
+ if (strand === -1 && matchingExonIdx > 0) {
795
+ for (let i = matchingExonIdx - 1; i >= 0; i--) {
796
+ const nextLoc = transcriptExonParts[i]
797
+ if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
798
+ nextExon = nextLoc
799
+ break
800
+ }
801
+ }
802
+ }
803
+ }
804
+ return { matchingExon, prevExon, nextExon }
213
805
  }
214
806
 
215
807
  const getFivePrimeSpliceSite = (
@@ -265,12 +857,17 @@ export const TranscriptWidgetEditLocation = observer(
265
857
  const getTranslationSequence = () => {
266
858
  let wholeSequence = ''
267
859
  const [firstLocation] = cdsLocations
268
- for (const loc of firstLocation) {
269
- let sequence = refData.getSequence(loc.min, loc.max)
270
- if (strand === -1) {
271
- sequence = revcom(sequence)
272
- }
273
- wholeSequence += sequence
860
+ const sortedCDSLocations = firstLocation.toSorted(
861
+ ({ min: a }, { min: b }) => a - b,
862
+ )
863
+ for (const loc of sortedCDSLocations) {
864
+ wholeSequence += refData.getSequence(loc.min, loc.max)
865
+ }
866
+ if (strand === -1) {
867
+ // Original: ACGCAT
868
+ // Complement: TGCGTA
869
+ // Reverse complement: ATGCGT
870
+ wholeSequence = revcom(wholeSequence)
274
871
  }
275
872
  const elements = []
276
873
  for (
@@ -299,15 +896,23 @@ export const TranscriptWidgetEditLocation = observer(
299
896
  // of the start codon. We are using the codonGenomicPos as the key in the typography
300
897
  // elements to maintain the genomic postion of the codon start
301
898
  const startCodonGenomicLocation =
302
- getStartCodonGenomicLocation(codonGenomicPos)
303
- if (startCodonGenomicLocation !== cdsMin) {
304
- handleCDSLocationChange(
899
+ getCodonGenomicLocation(codonGenomicPos)
900
+ if (startCodonGenomicLocation !== cdsMin && strand === 1) {
901
+ updateCDSLocation(
305
902
  cdsMin,
306
903
  startCodonGenomicLocation,
307
904
  feature,
308
905
  true,
309
906
  )
310
907
  }
908
+ if (startCodonGenomicLocation !== cdsMax && strand === -1) {
909
+ updateCDSLocation(
910
+ cdsMax,
911
+ startCodonGenomicLocation,
912
+ feature,
913
+ false,
914
+ )
915
+ }
311
916
  }}
312
917
  >
313
918
  {protein}
@@ -338,34 +943,41 @@ export const TranscriptWidgetEditLocation = observer(
338
943
 
339
944
  // Codon position is the index of the start codon in the CDS genomic sequence
340
945
  // Calculate the genomic location of the start codon based on the codon position in the CDS
341
- const getStartCodonGenomicLocation = (codonGenomicPosition: number) => {
946
+ const getCodonGenomicLocation = (codonGenomicPosition: number) => {
342
947
  const [firstLocation] = cdsLocations
343
948
  let cdsLen = 0
344
- for (const loc of firstLocation) {
345
- const locLength = loc.max - loc.min
346
- // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
347
- // and codonGenomicPosition is 25
348
- // (((10 - 0) + (30 - 20)) + 10) > 25
349
- // 40 + (25-20) = 45 is the genomic location of the start codon
350
- if (cdsLen + locLength > codonGenomicPosition) {
351
- return loc.min + (codonGenomicPosition - cdsLen)
352
- }
353
- cdsLen += locLength
354
- }
355
- return cdsMin
356
- }
949
+ const sortedCDSLocations = firstLocation.toSorted(
950
+ ({ min: a }, { min: b }) => a - b,
951
+ )
357
952
 
358
- const getStopCodonGenomicLocation = (codonGenomicPosition: number) => {
359
- const [firstLocation] = cdsLocations
360
- let cdsLen = 0
361
- for (const loc of firstLocation) {
362
- const locLength = loc.max - loc.min
363
- // Check if the codonPosition is within the current location
364
- if (cdsLen + locLength > codonGenomicPosition) {
365
- return loc.min + (codonGenomicPosition - cdsLen)
953
+ // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
954
+ // and codonGenomicPosition is 25
955
+ // ((10 - 0) + (30 - 20) + (50 - 40)) > 25
956
+ // So, start codon is in (40, 50)
957
+ // 40 + (25-20) = 45 is the genomic location of the start codon
958
+ if (strand === 1) {
959
+ for (const loc of sortedCDSLocations) {
960
+ const locLength = loc.max - loc.min
961
+ if (cdsLen + locLength > codonGenomicPosition) {
962
+ return loc.min + (codonGenomicPosition - cdsLen)
963
+ }
964
+ cdsLen += locLength
965
+ }
966
+ } else if (strand === -1) {
967
+ for (let i = sortedCDSLocations.length - 1; i >= 0; i--) {
968
+ const loc = sortedCDSLocations[i]
969
+ const locLength = loc.max - loc.min
970
+ if (cdsLen + locLength > codonGenomicPosition) {
971
+ return loc.max - (codonGenomicPosition - cdsLen)
972
+ }
973
+ cdsLen += locLength
366
974
  }
367
- cdsLen += locLength
368
975
  }
976
+
977
+ if (strand === 1) {
978
+ return cdsMin
979
+ }
980
+
369
981
  return cdsMax
370
982
  }
371
983
 
@@ -396,9 +1008,10 @@ export const TranscriptWidgetEditLocation = observer(
396
1008
  return
397
1009
  }
398
1010
 
399
- // Trim any sequence before first start codon and after last stop codon
1011
+ // Trim any sequence before first start codon and after stop codon
400
1012
  const startCodonIndex = translationSequence.indexOf('M')
401
- const stopCodonIndex = translationSequence.lastIndexOf('*') + 1
1013
+ const stopCodonIndex = translationSequence.indexOf('*') + 1
1014
+
402
1015
  const startCodonPos =
403
1016
  translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
404
1017
  const stopCodonPos =
@@ -407,46 +1020,97 @@ export const TranscriptWidgetEditLocation = observer(
407
1020
  if (!startCodonPos || !stopCodonPos) {
408
1021
  return
409
1022
  }
410
-
411
- const startCodonGenomicLoc = getStartCodonGenomicLocation(
1023
+ const startCodonGenomicLoc = getCodonGenomicLocation(
412
1024
  startCodonPos as unknown as number,
413
1025
  )
414
- const stopCodonGenomicLoc = getStopCodonGenomicLocation(
1026
+ const stopCodonGenomicLoc = getCodonGenomicLocation(
415
1027
  stopCodonPos as unknown as number,
416
1028
  )
417
1029
 
418
- if (startCodonGenomicLoc !== cdsMin) {
419
- handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true)
1030
+ if (strand === 1) {
1031
+ if (startCodonGenomicLoc > stopCodonGenomicLoc) {
1032
+ notify(
1033
+ 'Start codon genomic location should be less than stop codon genomic location',
1034
+ 'error',
1035
+ )
1036
+ return
1037
+ }
1038
+ let promise
1039
+ if (startCodonGenomicLoc !== cdsMin) {
1040
+ promise = new Promise((resolve) => {
1041
+ updateCDSLocation(
1042
+ cdsMin,
1043
+ startCodonGenomicLoc,
1044
+ feature,
1045
+ true,
1046
+ () => {
1047
+ resolve(true)
1048
+ },
1049
+ )
1050
+ })
1051
+ }
1052
+
1053
+ if (stopCodonGenomicLoc !== cdsMax) {
1054
+ if (promise) {
1055
+ void promise.then(() => {
1056
+ updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
1057
+ })
1058
+ } else {
1059
+ updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
1060
+ }
1061
+ }
420
1062
  }
421
1063
 
422
- if (stopCodonGenomicLoc !== cdsMax) {
423
- // TODO: getting error when trying to change the CDS start and end location at the same time
424
- // Need to fix this
425
- setTimeout(() => {
426
- handleCDSLocationChange(cdsMax, stopCodonGenomicLoc, feature, false)
427
- }, 1000)
1064
+ if (strand === -1) {
1065
+ // reverse strand
1066
+ if (startCodonGenomicLoc < stopCodonGenomicLoc) {
1067
+ notify(
1068
+ 'Start codon genomic location should be less than stop codon genomic location',
1069
+ 'error',
1070
+ )
1071
+ return
1072
+ }
1073
+ let promise
1074
+ if (startCodonGenomicLoc !== cdsMax) {
1075
+ promise = new Promise((resolve) => {
1076
+ updateCDSLocation(
1077
+ cdsMax,
1078
+ startCodonGenomicLoc,
1079
+ feature,
1080
+ false,
1081
+ () => {
1082
+ resolve(true)
1083
+ },
1084
+ )
1085
+ })
1086
+ }
1087
+
1088
+ if (stopCodonGenomicLoc !== cdsMin) {
1089
+ if (promise) {
1090
+ void promise.then(() => {
1091
+ updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
1092
+ })
1093
+ } else {
1094
+ updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
1095
+ }
1096
+ }
428
1097
  }
1098
+ notify('Translation sequence trimmed to start and stop codons', 'success')
429
1099
  }
430
1100
 
431
- const copyToClipboard = () => {
1101
+ const onCopyClick = () => {
432
1102
  const seqDiv = seqRef.current
433
1103
  if (!seqDiv) {
434
1104
  return
435
1105
  }
436
- const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' })
437
- const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' })
438
- const clipboardItem = new ClipboardItem({
439
- [textBlob.type]: textBlob,
440
- [htmlBlob.type]: htmlBlob,
441
- })
442
- void navigator.clipboard.write([clipboardItem])
1106
+ void copyToClipboard(seqDiv)
443
1107
  }
444
1108
 
445
1109
  return (
446
1110
  <div>
447
1111
  {cdsPresent && (
448
1112
  <div>
449
- <Accordion defaultExpanded>
1113
+ <Accordion>
450
1114
  <StyledAccordionSummary
451
1115
  expandIcon={<ExpandMoreIcon style={{ color: 'white' }} />}
452
1116
  aria-controls="panel1-content"
@@ -458,7 +1122,11 @@ export const TranscriptWidgetEditLocation = observer(
458
1122
  </StyledAccordionSummary>
459
1123
  <AccordionDetails>
460
1124
  <SequenceContainer>
461
- <Typography component={'span'} ref={seqRef}>
1125
+ <Typography
1126
+ component={'span'}
1127
+ ref={seqRef}
1128
+ style={{ maxHeight: 120, overflowY: 'scroll' }}
1129
+ >
462
1130
  {getTranslationSequence()}
463
1131
  </Typography>
464
1132
  </SequenceContainer>
@@ -474,7 +1142,7 @@ export const TranscriptWidgetEditLocation = observer(
474
1142
  <Tooltip title="Copy">
475
1143
  <ContentCopyIcon
476
1144
  style={{ fontSize: 15, cursor: 'pointer' }}
477
- onClick={copyToClipboard}
1145
+ onClick={onCopyClick}
478
1146
  />
479
1147
  </Tooltip>
480
1148
  <Tooltip title="Trim">
@@ -493,29 +1161,79 @@ export const TranscriptWidgetEditLocation = observer(
493
1161
  style={{ textAlign: 'center', marginTop: 10 }}
494
1162
  >
495
1163
  <Grid2 size={1} />
496
- <Grid2 size={4}>
497
- <StyledTextField
498
- margin="dense"
499
- variant="outlined"
500
- value={cdsMin}
501
- onChangeCommitted={(newLocation: number) => {
502
- handleCDSLocationChange(cdsMin, newLocation, feature, true)
503
- }}
504
- />
505
- </Grid2>
1164
+ {strand === 1 ? (
1165
+ <Grid2 size={4}>
1166
+ <StyledTextField
1167
+ margin="dense"
1168
+ variant="outlined"
1169
+ value={cdsMin + 1}
1170
+ onChangeCommitted={(newLocation: number) => {
1171
+ handleCDSLocationChange(
1172
+ cdsMin,
1173
+ newLocation - 1,
1174
+ feature,
1175
+ true,
1176
+ )
1177
+ }}
1178
+ style={{ border: '1px solid black', borderRadius: 5 }}
1179
+ />
1180
+ </Grid2>
1181
+ ) : (
1182
+ <Grid2 size={4}>
1183
+ <StyledTextField
1184
+ margin="dense"
1185
+ variant="outlined"
1186
+ value={cdsMax}
1187
+ onChangeCommitted={(newLocation: number) => {
1188
+ handleCDSLocationChange(
1189
+ cdsMax,
1190
+ newLocation,
1191
+ feature,
1192
+ false,
1193
+ )
1194
+ }}
1195
+ style={{ border: '1px solid black', borderRadius: 5 }}
1196
+ />
1197
+ </Grid2>
1198
+ )}
506
1199
  <Grid2 size={2}>
507
1200
  <Typography component={'span'}>CDS</Typography>
508
1201
  </Grid2>
509
- <Grid2 size={4}>
510
- <StyledTextField
511
- margin="dense"
512
- variant="outlined"
513
- value={cdsMax}
514
- onChangeCommitted={(newLocation: number) => {
515
- handleCDSLocationChange(cdsMax, newLocation, feature, false)
516
- }}
517
- />
518
- </Grid2>
1202
+ {strand === 1 ? (
1203
+ <Grid2 size={4}>
1204
+ <StyledTextField
1205
+ margin="dense"
1206
+ variant="outlined"
1207
+ value={cdsMax}
1208
+ onChangeCommitted={(newLocation: number) => {
1209
+ handleCDSLocationChange(
1210
+ cdsMax,
1211
+ newLocation,
1212
+ feature,
1213
+ false,
1214
+ )
1215
+ }}
1216
+ style={{ border: '1px solid black', borderRadius: 5 }}
1217
+ />
1218
+ </Grid2>
1219
+ ) : (
1220
+ <Grid2 size={4}>
1221
+ <StyledTextField
1222
+ margin="dense"
1223
+ variant="outlined"
1224
+ value={cdsMin + 1}
1225
+ onChangeCommitted={(newLocation: number) => {
1226
+ handleCDSLocationChange(
1227
+ cdsMin,
1228
+ newLocation - 1,
1229
+ feature,
1230
+ true,
1231
+ )
1232
+ }}
1233
+ style={{ border: '1px solid black', borderRadius: 5 }}
1234
+ />
1235
+ </Grid2>
1236
+ )}
519
1237
  <Grid2 size={1} />
520
1238
  </Grid2>
521
1239
  </div>
@@ -543,39 +1261,75 @@ export const TranscriptWidgetEditLocation = observer(
543
1261
  </Typography>
544
1262
  ))}
545
1263
  </Grid2>
546
- <Grid2 size={4} style={{ padding: 0 }}>
547
- <StyledTextField
548
- margin="dense"
549
- variant="outlined"
550
- value={loc.min}
551
- onChangeCommitted={(newLocation: number) => {
552
- handleExonLocationChange(
553
- loc.min,
554
- newLocation,
555
- feature,
556
- true,
557
- )
558
- }}
559
- />
560
- </Grid2>
1264
+ {strand === 1 ? (
1265
+ <Grid2 size={4} style={{ padding: 0 }}>
1266
+ <StyledTextField
1267
+ margin="dense"
1268
+ variant="outlined"
1269
+ value={loc.min + 1}
1270
+ onChangeCommitted={(newLocation: number) => {
1271
+ handleExonLocationChange(
1272
+ loc.min,
1273
+ newLocation - 1,
1274
+ feature,
1275
+ true,
1276
+ )
1277
+ }}
1278
+ />
1279
+ </Grid2>
1280
+ ) : (
1281
+ <Grid2 size={4} style={{ padding: 0 }}>
1282
+ <StyledTextField
1283
+ margin="dense"
1284
+ variant="outlined"
1285
+ value={loc.max}
1286
+ onChangeCommitted={(newLocation: number) => {
1287
+ handleExonLocationChange(
1288
+ loc.max,
1289
+ newLocation,
1290
+ feature,
1291
+ false,
1292
+ )
1293
+ }}
1294
+ />
1295
+ </Grid2>
1296
+ )}
561
1297
  <Grid2 size={2}>
562
1298
  <Strand strand={feature.strand} />
563
1299
  </Grid2>
564
- <Grid2 size={4} style={{ padding: 0 }}>
565
- <StyledTextField
566
- margin="dense"
567
- variant="outlined"
568
- value={loc.max}
569
- onChangeCommitted={(newLocation: number) => {
570
- handleExonLocationChange(
571
- loc.max,
572
- newLocation,
573
- feature,
574
- false,
575
- )
576
- }}
577
- />
578
- </Grid2>
1300
+ {strand === 1 ? (
1301
+ <Grid2 size={4} style={{ padding: 0 }}>
1302
+ <StyledTextField
1303
+ margin="dense"
1304
+ variant="outlined"
1305
+ value={loc.max}
1306
+ onChangeCommitted={(newLocation: number) => {
1307
+ handleExonLocationChange(
1308
+ loc.max,
1309
+ newLocation,
1310
+ feature,
1311
+ false,
1312
+ )
1313
+ }}
1314
+ />
1315
+ </Grid2>
1316
+ ) : (
1317
+ <Grid2 size={4} style={{ padding: 0 }}>
1318
+ <StyledTextField
1319
+ margin="dense"
1320
+ variant="outlined"
1321
+ value={loc.min + 1}
1322
+ onChangeCommitted={(newLocation: number) => {
1323
+ handleExonLocationChange(
1324
+ loc.min,
1325
+ newLocation - 1,
1326
+ feature,
1327
+ true,
1328
+ )
1329
+ }}
1330
+ />
1331
+ </Grid2>
1332
+ )}
579
1333
  <Grid2 size={1}>
580
1334
  {index !== transcriptExonParts.length - 1 &&
581
1335
  getThreePrimeSpliceSite(loc, index).map((site, idx) => (