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

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 (84) hide show
  1. package/dist/index.esm.js +4603 -2045
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
  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 +9387 -4016
  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 +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +42 -18
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
  18. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
  19. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  20. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
  21. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
  22. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
  23. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
  24. package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
  25. package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
  26. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  27. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
  28. package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
  29. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  34. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  36. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
  37. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  38. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
  39. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  40. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
  41. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
  42. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
  43. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
  44. package/src/OntologyManager/index.ts +4 -1
  45. package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
  46. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  47. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  48. package/src/components/AddAssembly.tsx +1 -1
  49. package/src/components/AddAssemblyAliases.tsx +114 -0
  50. package/src/components/AddChildFeature.tsx +7 -7
  51. package/src/components/AddFeature.tsx +20 -15
  52. package/src/components/AddRefSeqAliases.tsx +9 -9
  53. package/src/components/CopyFeature.tsx +4 -4
  54. package/src/components/CreateApolloAnnotation.tsx +335 -151
  55. package/src/components/DeleteAssembly.tsx +1 -1
  56. package/src/components/DeleteFeature.tsx +358 -11
  57. package/src/components/DownloadGFF3.tsx +20 -1
  58. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  59. package/src/components/FilterFeatures.tsx +7 -7
  60. package/src/components/FilterTranscripts.tsx +86 -0
  61. package/src/components/ImportFeatures.tsx +1 -1
  62. package/src/components/ManageChecks.tsx +1 -1
  63. package/src/components/MergeExons.tsx +193 -0
  64. package/src/components/MergeTranscripts.tsx +182 -0
  65. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  66. package/src/components/OpenLocalFile.tsx +11 -7
  67. package/src/components/SplitExon.tsx +134 -0
  68. package/src/components/ViewCheckResults.tsx +1 -1
  69. package/src/components/index.ts +4 -0
  70. package/src/config.ts +11 -0
  71. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  72. package/src/extensions/annotationFromPileup.ts +99 -89
  73. package/src/index.ts +42 -105
  74. package/src/makeDisplayComponent.tsx +0 -1
  75. package/src/menus/index.ts +1 -0
  76. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
  77. package/src/menus/topLevelMenuAdmin.ts +154 -0
  78. package/src/session/session.ts +163 -104
  79. package/src/util/annotationFeatureUtils.ts +59 -0
  80. package/src/util/copyToClipboard.ts +21 -0
  81. package/src/util/displayUtils.ts +149 -0
  82. package/src/util/glyphUtils.ts +201 -0
  83. package/src/util/index.ts +2 -0
  84. package/src/util/mouseEventsUtils.ts +145 -0
@@ -25,14 +25,16 @@ import RemoveIcon from '@mui/icons-material/Remove'
25
25
  import {
26
26
  Accordion,
27
27
  AccordionDetails,
28
- Grid2,
28
+ Grid,
29
29
  Tooltip,
30
30
  Typography,
31
31
  } from '@mui/material'
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)
116
291
  }
117
- if (isMin && oldLocation === child.min) {
118
- const change = new LocationStartChange({
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)
339
+ }
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 = (
@@ -229,6 +821,7 @@ export const TranscriptWidgetEditLocation = observer(
229
821
  }
230
822
  }
231
823
  }
824
+ spliceSite = spliceSite.toUpperCase()
232
825
  return [
233
826
  {
234
827
  spliceSite,
@@ -254,6 +847,7 @@ export const TranscriptWidgetEditLocation = observer(
254
847
  }
255
848
  }
256
849
  }
850
+ spliceSite = spliceSite.toUpperCase()
257
851
  return [
258
852
  {
259
853
  spliceSite,
@@ -265,12 +859,17 @@ export const TranscriptWidgetEditLocation = observer(
265
859
  const getTranslationSequence = () => {
266
860
  let wholeSequence = ''
267
861
  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
862
+ const sortedCDSLocations = firstLocation.toSorted(
863
+ ({ min: a }, { min: b }) => a - b,
864
+ )
865
+ for (const loc of sortedCDSLocations) {
866
+ wholeSequence += refData.getSequence(loc.min, loc.max)
867
+ }
868
+ if (strand === -1) {
869
+ // Original: ACGCAT
870
+ // Complement: TGCGTA
871
+ // Reverse complement: ATGCGT
872
+ wholeSequence = revcom(wholeSequence)
274
873
  }
275
874
  const elements = []
276
875
  for (
@@ -299,15 +898,23 @@ export const TranscriptWidgetEditLocation = observer(
299
898
  // of the start codon. We are using the codonGenomicPos as the key in the typography
300
899
  // elements to maintain the genomic postion of the codon start
301
900
  const startCodonGenomicLocation =
302
- getStartCodonGenomicLocation(codonGenomicPos)
303
- if (startCodonGenomicLocation !== cdsMin) {
304
- handleCDSLocationChange(
901
+ getCodonGenomicLocation(codonGenomicPos)
902
+ if (startCodonGenomicLocation !== cdsMin && strand === 1) {
903
+ updateCDSLocation(
305
904
  cdsMin,
306
905
  startCodonGenomicLocation,
307
906
  feature,
308
907
  true,
309
908
  )
310
909
  }
910
+ if (startCodonGenomicLocation !== cdsMax && strand === -1) {
911
+ updateCDSLocation(
912
+ cdsMax,
913
+ startCodonGenomicLocation,
914
+ feature,
915
+ false,
916
+ )
917
+ }
311
918
  }}
312
919
  >
313
920
  {protein}
@@ -338,34 +945,41 @@ export const TranscriptWidgetEditLocation = observer(
338
945
 
339
946
  // Codon position is the index of the start codon in the CDS genomic sequence
340
947
  // Calculate the genomic location of the start codon based on the codon position in the CDS
341
- const getStartCodonGenomicLocation = (codonGenomicPosition: number) => {
948
+ const getCodonGenomicLocation = (codonGenomicPosition: number) => {
342
949
  const [firstLocation] = cdsLocations
343
950
  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
- }
951
+ const sortedCDSLocations = firstLocation.toSorted(
952
+ ({ min: a }, { min: b }) => a - b,
953
+ )
357
954
 
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)
955
+ // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
956
+ // and codonGenomicPosition is 25
957
+ // ((10 - 0) + (30 - 20) + (50 - 40)) > 25
958
+ // So, start codon is in (40, 50)
959
+ // 40 + (25-20) = 45 is the genomic location of the start codon
960
+ if (strand === 1) {
961
+ for (const loc of sortedCDSLocations) {
962
+ const locLength = loc.max - loc.min
963
+ if (cdsLen + locLength > codonGenomicPosition) {
964
+ return loc.min + (codonGenomicPosition - cdsLen)
965
+ }
966
+ cdsLen += locLength
366
967
  }
367
- cdsLen += locLength
968
+ } else if (strand === -1) {
969
+ for (let i = sortedCDSLocations.length - 1; i >= 0; i--) {
970
+ const loc = sortedCDSLocations[i]
971
+ const locLength = loc.max - loc.min
972
+ if (cdsLen + locLength > codonGenomicPosition) {
973
+ return loc.max - (codonGenomicPosition - cdsLen)
974
+ }
975
+ cdsLen += locLength
976
+ }
977
+ }
978
+
979
+ if (strand === 1) {
980
+ return cdsMin
368
981
  }
982
+
369
983
  return cdsMax
370
984
  }
371
985
 
@@ -396,9 +1010,10 @@ export const TranscriptWidgetEditLocation = observer(
396
1010
  return
397
1011
  }
398
1012
 
399
- // Trim any sequence before first start codon and after last stop codon
1013
+ // Trim any sequence before first start codon and after stop codon
400
1014
  const startCodonIndex = translationSequence.indexOf('M')
401
- const stopCodonIndex = translationSequence.lastIndexOf('*') + 1
1015
+ const stopCodonIndex = translationSequence.indexOf('*') + 1
1016
+
402
1017
  const startCodonPos =
403
1018
  translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
404
1019
  const stopCodonPos =
@@ -407,46 +1022,97 @@ export const TranscriptWidgetEditLocation = observer(
407
1022
  if (!startCodonPos || !stopCodonPos) {
408
1023
  return
409
1024
  }
410
-
411
- const startCodonGenomicLoc = getStartCodonGenomicLocation(
1025
+ const startCodonGenomicLoc = getCodonGenomicLocation(
412
1026
  startCodonPos as unknown as number,
413
1027
  )
414
- const stopCodonGenomicLoc = getStopCodonGenomicLocation(
1028
+ const stopCodonGenomicLoc = getCodonGenomicLocation(
415
1029
  stopCodonPos as unknown as number,
416
1030
  )
417
1031
 
418
- if (startCodonGenomicLoc !== cdsMin) {
419
- handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true)
1032
+ if (strand === 1) {
1033
+ if (startCodonGenomicLoc > stopCodonGenomicLoc) {
1034
+ notify(
1035
+ 'Start codon genomic location should be less than stop codon genomic location',
1036
+ 'error',
1037
+ )
1038
+ return
1039
+ }
1040
+ let promise
1041
+ if (startCodonGenomicLoc !== cdsMin) {
1042
+ promise = new Promise((resolve) => {
1043
+ updateCDSLocation(
1044
+ cdsMin,
1045
+ startCodonGenomicLoc,
1046
+ feature,
1047
+ true,
1048
+ () => {
1049
+ resolve(true)
1050
+ },
1051
+ )
1052
+ })
1053
+ }
1054
+
1055
+ if (stopCodonGenomicLoc !== cdsMax) {
1056
+ if (promise) {
1057
+ void promise.then(() => {
1058
+ updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
1059
+ })
1060
+ } else {
1061
+ updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
1062
+ }
1063
+ }
420
1064
  }
421
1065
 
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)
1066
+ if (strand === -1) {
1067
+ // reverse strand
1068
+ if (startCodonGenomicLoc < stopCodonGenomicLoc) {
1069
+ notify(
1070
+ 'Start codon genomic location should be less than stop codon genomic location',
1071
+ 'error',
1072
+ )
1073
+ return
1074
+ }
1075
+ let promise
1076
+ if (startCodonGenomicLoc !== cdsMax) {
1077
+ promise = new Promise((resolve) => {
1078
+ updateCDSLocation(
1079
+ cdsMax,
1080
+ startCodonGenomicLoc,
1081
+ feature,
1082
+ false,
1083
+ () => {
1084
+ resolve(true)
1085
+ },
1086
+ )
1087
+ })
1088
+ }
1089
+
1090
+ if (stopCodonGenomicLoc !== cdsMin) {
1091
+ if (promise) {
1092
+ void promise.then(() => {
1093
+ updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
1094
+ })
1095
+ } else {
1096
+ updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
1097
+ }
1098
+ }
428
1099
  }
1100
+ notify('Translation sequence trimmed to start and stop codons', 'success')
429
1101
  }
430
1102
 
431
- const copyToClipboard = () => {
1103
+ const onCopyClick = () => {
432
1104
  const seqDiv = seqRef.current
433
1105
  if (!seqDiv) {
434
1106
  return
435
1107
  }
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])
1108
+ void copyToClipboard(seqDiv)
443
1109
  }
444
1110
 
445
1111
  return (
446
1112
  <div>
447
1113
  {cdsPresent && (
448
1114
  <div>
449
- <Accordion defaultExpanded>
1115
+ <Accordion>
450
1116
  <StyledAccordionSummary
451
1117
  expandIcon={<ExpandMoreIcon style={{ color: 'white' }} />}
452
1118
  aria-controls="panel1-content"
@@ -458,7 +1124,11 @@ export const TranscriptWidgetEditLocation = observer(
458
1124
  </StyledAccordionSummary>
459
1125
  <AccordionDetails>
460
1126
  <SequenceContainer>
461
- <Typography component={'span'} ref={seqRef}>
1127
+ <Typography
1128
+ component={'span'}
1129
+ ref={seqRef}
1130
+ style={{ maxHeight: 120, overflowY: 'scroll' }}
1131
+ >
462
1132
  {getTranslationSequence()}
463
1133
  </Typography>
464
1134
  </SequenceContainer>
@@ -474,7 +1144,7 @@ export const TranscriptWidgetEditLocation = observer(
474
1144
  <Tooltip title="Copy">
475
1145
  <ContentCopyIcon
476
1146
  style={{ fontSize: 15, cursor: 'pointer' }}
477
- onClick={copyToClipboard}
1147
+ onClick={onCopyClick}
478
1148
  />
479
1149
  </Tooltip>
480
1150
  <Tooltip title="Trim">
@@ -486,38 +1156,88 @@ export const TranscriptWidgetEditLocation = observer(
486
1156
  </div>
487
1157
  </AccordionDetails>
488
1158
  </Accordion>
489
- <Grid2
1159
+ <Grid
490
1160
  container
491
1161
  justifyContent="center"
492
1162
  alignItems="center"
493
1163
  style={{ textAlign: 'center', marginTop: 10 }}
494
1164
  >
495
- <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>
506
- <Grid2 size={2}>
1165
+ <Grid size={1} />
1166
+ {strand === 1 ? (
1167
+ <Grid size={4}>
1168
+ <StyledTextField
1169
+ margin="dense"
1170
+ variant="outlined"
1171
+ value={cdsMin + 1}
1172
+ onChangeCommitted={(newLocation: number) => {
1173
+ handleCDSLocationChange(
1174
+ cdsMin,
1175
+ newLocation - 1,
1176
+ feature,
1177
+ true,
1178
+ )
1179
+ }}
1180
+ style={{ border: '1px solid black', borderRadius: 5 }}
1181
+ />
1182
+ </Grid>
1183
+ ) : (
1184
+ <Grid size={4}>
1185
+ <StyledTextField
1186
+ margin="dense"
1187
+ variant="outlined"
1188
+ value={cdsMax}
1189
+ onChangeCommitted={(newLocation: number) => {
1190
+ handleCDSLocationChange(
1191
+ cdsMax,
1192
+ newLocation,
1193
+ feature,
1194
+ false,
1195
+ )
1196
+ }}
1197
+ style={{ border: '1px solid black', borderRadius: 5 }}
1198
+ />
1199
+ </Grid>
1200
+ )}
1201
+ <Grid size={2}>
507
1202
  <Typography component={'span'}>CDS</Typography>
508
- </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>
519
- <Grid2 size={1} />
520
- </Grid2>
1203
+ </Grid>
1204
+ {strand === 1 ? (
1205
+ <Grid size={4}>
1206
+ <StyledTextField
1207
+ margin="dense"
1208
+ variant="outlined"
1209
+ value={cdsMax}
1210
+ onChangeCommitted={(newLocation: number) => {
1211
+ handleCDSLocationChange(
1212
+ cdsMax,
1213
+ newLocation,
1214
+ feature,
1215
+ false,
1216
+ )
1217
+ }}
1218
+ style={{ border: '1px solid black', borderRadius: 5 }}
1219
+ />
1220
+ </Grid>
1221
+ ) : (
1222
+ <Grid size={4}>
1223
+ <StyledTextField
1224
+ margin="dense"
1225
+ variant="outlined"
1226
+ value={cdsMin + 1}
1227
+ onChangeCommitted={(newLocation: number) => {
1228
+ handleCDSLocationChange(
1229
+ cdsMin,
1230
+ newLocation - 1,
1231
+ feature,
1232
+ true,
1233
+ )
1234
+ }}
1235
+ style={{ border: '1px solid black', borderRadius: 5 }}
1236
+ />
1237
+ </Grid>
1238
+ )}
1239
+ <Grid size={1} />
1240
+ </Grid>
521
1241
  </div>
522
1242
  )}
523
1243
  <div style={{ marginTop: 5 }}>
@@ -525,13 +1245,13 @@ export const TranscriptWidgetEditLocation = observer(
525
1245
  return (
526
1246
  <div key={index}>
527
1247
  {loc.type === 'exon' && (
528
- <Grid2
1248
+ <Grid
529
1249
  container
530
1250
  justifyContent="center"
531
1251
  alignItems="center"
532
1252
  style={{ textAlign: 'center' }}
533
1253
  >
534
- <Grid2 size={1}>
1254
+ <Grid size={1}>
535
1255
  {index !== 0 &&
536
1256
  getFivePrimeSpliceSite(loc, index).map((site, idx) => (
537
1257
  <Typography
@@ -542,41 +1262,77 @@ export const TranscriptWidgetEditLocation = observer(
542
1262
  {site.spliceSite}
543
1263
  </Typography>
544
1264
  ))}
545
- </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>
561
- <Grid2 size={2}>
1265
+ </Grid>
1266
+ {strand === 1 ? (
1267
+ <Grid size={4} style={{ padding: 0 }}>
1268
+ <StyledTextField
1269
+ margin="dense"
1270
+ variant="outlined"
1271
+ value={loc.min + 1}
1272
+ onChangeCommitted={(newLocation: number) => {
1273
+ handleExonLocationChange(
1274
+ loc.min,
1275
+ newLocation - 1,
1276
+ feature,
1277
+ true,
1278
+ )
1279
+ }}
1280
+ />
1281
+ </Grid>
1282
+ ) : (
1283
+ <Grid size={4} style={{ padding: 0 }}>
1284
+ <StyledTextField
1285
+ margin="dense"
1286
+ variant="outlined"
1287
+ value={loc.max}
1288
+ onChangeCommitted={(newLocation: number) => {
1289
+ handleExonLocationChange(
1290
+ loc.max,
1291
+ newLocation,
1292
+ feature,
1293
+ false,
1294
+ )
1295
+ }}
1296
+ />
1297
+ </Grid>
1298
+ )}
1299
+ <Grid size={2}>
562
1300
  <Strand strand={feature.strand} />
563
- </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>
579
- <Grid2 size={1}>
1301
+ </Grid>
1302
+ {strand === 1 ? (
1303
+ <Grid size={4} style={{ padding: 0 }}>
1304
+ <StyledTextField
1305
+ margin="dense"
1306
+ variant="outlined"
1307
+ value={loc.max}
1308
+ onChangeCommitted={(newLocation: number) => {
1309
+ handleExonLocationChange(
1310
+ loc.max,
1311
+ newLocation,
1312
+ feature,
1313
+ false,
1314
+ )
1315
+ }}
1316
+ />
1317
+ </Grid>
1318
+ ) : (
1319
+ <Grid size={4} style={{ padding: 0 }}>
1320
+ <StyledTextField
1321
+ margin="dense"
1322
+ variant="outlined"
1323
+ value={loc.min + 1}
1324
+ onChangeCommitted={(newLocation: number) => {
1325
+ handleExonLocationChange(
1326
+ loc.min,
1327
+ newLocation - 1,
1328
+ feature,
1329
+ true,
1330
+ )
1331
+ }}
1332
+ />
1333
+ </Grid>
1334
+ )}
1335
+ <Grid size={1}>
580
1336
  {index !== transcriptExonParts.length - 1 &&
581
1337
  getThreePrimeSpliceSite(loc, index).map((site, idx) => (
582
1338
  <Typography
@@ -587,8 +1343,8 @@ export const TranscriptWidgetEditLocation = observer(
587
1343
  {site.spliceSite}
588
1344
  </Typography>
589
1345
  ))}
590
- </Grid2>
591
- </Grid2>
1346
+ </Grid>
1347
+ </Grid>
592
1348
  )}
593
1349
  </div>
594
1350
  )