@apollo-annotation/jbrowse-plugin-apollo 0.3.4 → 0.3.5

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 (44) hide show
  1. package/dist/index.esm.js +1513 -1074
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +1510 -1065
  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 +4681 -2097
  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/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +13 -0
  13. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +88 -17
  14. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +144 -22
  15. package/src/FeatureDetailsWidget/Attributes.tsx +93 -43
  16. package/src/FeatureDetailsWidget/BasicInformation.tsx +2 -4
  17. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +6 -4
  18. package/src/FeatureDetailsWidget/Sequence.tsx +16 -33
  19. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +137 -92
  20. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +600 -0
  21. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +54 -0
  22. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +13 -6
  23. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +6 -27
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +35 -13
  25. package/src/LinearApolloDisplay/stateModel/layouts.ts +7 -2
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +4 -0
  27. package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +10 -1
  28. package/src/OntologyManager/OntologyStore/index.test.ts +1 -0
  29. package/src/OntologyManager/index.ts +2 -0
  30. package/src/SixFrameFeatureDisplay/stateModel.ts +5 -1
  31. package/src/TabularEditor/HybridGrid/Feature.tsx +0 -1
  32. package/src/TabularEditor/HybridGrid/NumberCell.tsx +8 -1
  33. package/src/TabularEditor/HybridGrid/ToolBar.tsx +14 -12
  34. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +3 -27
  35. package/src/components/AddAssembly.tsx +608 -291
  36. package/src/components/CreateApolloAnnotation.tsx +144 -37
  37. package/src/components/OntologyTermMultiSelect.tsx +3 -0
  38. package/src/components/index.ts +0 -1
  39. package/src/extensions/annotationFromJBrowseFeature.ts +3 -2
  40. package/src/makeDisplayComponent.tsx +3 -4
  41. package/src/util/annotationFeatureUtils.ts +53 -0
  42. package/src/util/index.ts +1 -0
  43. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +0 -200
  44. package/src/components/ModifyFeatureAttribute.tsx +0 -460
@@ -0,0 +1,600 @@
1
+ /* eslint-disable unicorn/no-nested-ternary */
2
+ /* eslint-disable unicorn/prefer-at */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
6
+ import React, { useRef } from 'react'
7
+
8
+ import { observer } from 'mobx-react'
9
+
10
+ import styled from '@emotion/styled'
11
+ import {
12
+ Accordion,
13
+ AccordionDetails,
14
+ Grid2,
15
+ Tooltip,
16
+ Typography,
17
+ } from '@mui/material'
18
+
19
+ import AddIcon from '@mui/icons-material/Add'
20
+ import RemoveIcon from '@mui/icons-material/Remove'
21
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
22
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
23
+ import ContentCutIcon from '@mui/icons-material/ContentCut'
24
+
25
+ import { AnnotationFeature, TranscriptPart } from '@apollo-annotation/mst'
26
+
27
+ import { StyledAccordionSummary } from './ApolloTranscriptDetailsWidget'
28
+ import { NumberTextField } from './NumberTextField'
29
+ import { ApolloSessionModel } from '../session'
30
+ import {
31
+ AbstractSessionModel,
32
+ defaultCodonTable,
33
+ revcom,
34
+ } from '@jbrowse/core/util'
35
+ import {
36
+ LocationEndChange,
37
+ LocationStartChange,
38
+ } from '@apollo-annotation/shared'
39
+
40
+ const StyledTextField = styled(NumberTextField)(() => ({
41
+ '&.MuiFormControl-root': {
42
+ marginTop: 0,
43
+ marginBottom: 0,
44
+ width: '100%',
45
+ },
46
+ '& .MuiInputBase-input': {
47
+ fontSize: 12,
48
+ height: 20,
49
+ padding: 1,
50
+ paddingLeft: 10,
51
+ },
52
+ }))
53
+
54
+ const SequenceContainer = styled('div')({
55
+ display: 'flex',
56
+ justifyContent: 'center',
57
+ alignItems: 'center',
58
+ textAlign: 'left',
59
+ width: '100%',
60
+ overflowWrap: 'break-word',
61
+ wordWrap: 'break-word',
62
+ wordBreak: 'break-all',
63
+ '& span': {
64
+ fontSize: 12,
65
+ },
66
+ })
67
+
68
+ const Strand = (props: { strand: 1 | -1 | undefined }) => {
69
+ const { strand } = props
70
+
71
+ return (
72
+ <div>
73
+ {strand === 1 ? (
74
+ <AddIcon />
75
+ ) : strand === -1 ? (
76
+ <RemoveIcon />
77
+ ) : (
78
+ <Typography component={'span'}>N/A</Typography>
79
+ )}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export const TranscriptWidgetEditLocation = observer(
85
+ function TranscriptWidgetEditLocation({
86
+ assembly,
87
+ feature,
88
+ refName,
89
+ session,
90
+ }: {
91
+ feature: AnnotationFeature
92
+ refName: string
93
+ session: ApolloSessionModel
94
+ assembly: string
95
+ }) {
96
+ const { notify } = session as unknown as AbstractSessionModel
97
+ const currentAssembly = session.apolloDataStore.assemblies.get(assembly)
98
+ const refData = currentAssembly?.getByRefName(refName)
99
+ const { changeManager } = session.apolloDataStore
100
+ const seqRef = useRef<HTMLDivElement>(null)
101
+
102
+ // Separate function to handle CDS location change
103
+ // because start of CDS and exon might be same
104
+ function handleCDSLocationChange(
105
+ oldLocation: number,
106
+ newLocation: number,
107
+ feature: AnnotationFeature,
108
+ isMin: boolean,
109
+ ) {
110
+ if (!feature.children) {
111
+ throw new Error('Transcript should have child features')
112
+ }
113
+ for (const [, child] of feature.children) {
114
+ if (child.type !== 'CDS') {
115
+ continue
116
+ }
117
+ if (isMin && oldLocation === child.min) {
118
+ const change = new LocationStartChange({
119
+ typeName: 'LocationStartChange',
120
+ changedIds: [child._id],
121
+ featureId: feature._id,
122
+ oldStart: child.min,
123
+ newStart: newLocation,
124
+ assembly,
125
+ })
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({
133
+ typeName: 'LocationEndChange',
134
+ changedIds: [child._id],
135
+ featureId: feature._id,
136
+ oldEnd: child.max,
137
+ newEnd: newLocation,
138
+ assembly,
139
+ })
140
+ changeManager.submit(change).catch(() => {
141
+ notify('Error updating feature start position', 'error')
142
+ })
143
+ return
144
+ }
145
+ }
146
+ }
147
+
148
+ function handleExonLocationChange(
149
+ oldLocation: number,
150
+ newLocation: number,
151
+ feature: AnnotationFeature,
152
+ isMin: boolean,
153
+ ) {
154
+ if (!feature.children) {
155
+ throw new Error('Transcript should have child features')
156
+ }
157
+ for (const [, child] of feature.children) {
158
+ if (child.type !== 'exon') {
159
+ continue
160
+ }
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
174
+ }
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
188
+ }
189
+ }
190
+ }
191
+
192
+ if (!refData) {
193
+ return null
194
+ }
195
+
196
+ const { cdsLocations, transcriptExonParts, strand } = feature
197
+ const [firstCDSLocation] = cdsLocations
198
+
199
+ const exonParts = transcriptExonParts
200
+ .filter((part) => part.type === 'exon')
201
+ .sort(({ min: a }, { min: b }) => a - b)
202
+
203
+ const exonMin: number = exonParts[0]?.min
204
+ const exonMax: number = exonParts[exonParts.length - 1]?.max
205
+
206
+ let cdsMin = exonMin
207
+ let cdsMax = exonMax
208
+ const cdsPresent = firstCDSLocation.length > 0
209
+
210
+ if (cdsPresent) {
211
+ cdsMin = firstCDSLocation[0].min
212
+ cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max
213
+ }
214
+
215
+ const getFivePrimeSpliceSite = (
216
+ loc: TranscriptPart,
217
+ prevLocIdx: number,
218
+ ) => {
219
+ let spliceSite = ''
220
+ if (prevLocIdx > 0) {
221
+ const prevLoc = transcriptExonParts[prevLocIdx - 1]
222
+ if (strand === 1) {
223
+ if (prevLoc.type === 'intron') {
224
+ spliceSite = refData.getSequence(loc.min - 2, loc.min)
225
+ }
226
+ } else {
227
+ if (prevLoc.type === 'intron') {
228
+ spliceSite = revcom(refData.getSequence(loc.max, loc.max + 2))
229
+ }
230
+ }
231
+ }
232
+ return [
233
+ {
234
+ spliceSite,
235
+ color: spliceSite === 'AG' ? 'green' : 'red',
236
+ },
237
+ ]
238
+ }
239
+
240
+ const getThreePrimeSpliceSite = (
241
+ loc: TranscriptPart,
242
+ nextLocIdx: number,
243
+ ) => {
244
+ let spliceSite = ''
245
+ if (nextLocIdx < transcriptExonParts.length - 1) {
246
+ const nextLoc = transcriptExonParts[nextLocIdx + 1]
247
+ if (strand === 1) {
248
+ if (nextLoc.type === 'intron') {
249
+ spliceSite = refData.getSequence(loc.max, loc.max + 2)
250
+ }
251
+ } else {
252
+ if (nextLoc.type === 'intron') {
253
+ spliceSite = revcom(refData.getSequence(loc.min - 2, loc.min))
254
+ }
255
+ }
256
+ }
257
+ return [
258
+ {
259
+ spliceSite,
260
+ color: spliceSite === 'GT' ? 'green' : 'red',
261
+ },
262
+ ]
263
+ }
264
+
265
+ const getTranslationSequence = () => {
266
+ let wholeSequence = ''
267
+ 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
274
+ }
275
+ const elements = []
276
+ for (
277
+ let codonGenomicPos = 0;
278
+ codonGenomicPos < wholeSequence.length;
279
+ codonGenomicPos += 3
280
+ ) {
281
+ const codonSeq = wholeSequence
282
+ .slice(codonGenomicPos, codonGenomicPos + 3)
283
+ .toUpperCase()
284
+ const protein =
285
+ defaultCodonTable[codonSeq as keyof typeof defaultCodonTable] || '&'
286
+ // highlight start codon and stop codons
287
+ if (codonSeq === 'ATG') {
288
+ elements.push(
289
+ <Typography
290
+ component={'span'}
291
+ style={{
292
+ backgroundColor: 'yellow',
293
+ cursor: 'pointer',
294
+ border: '1px solid black',
295
+ }}
296
+ key={codonGenomicPos}
297
+ onClick={() => {
298
+ // NOTE: codonGenomicPos is important here for calculating the genomic location
299
+ // of the start codon. We are using the codonGenomicPos as the key in the typography
300
+ // elements to maintain the genomic postion of the codon start
301
+ const startCodonGenomicLocation =
302
+ getStartCodonGenomicLocation(codonGenomicPos)
303
+ if (startCodonGenomicLocation !== cdsMin) {
304
+ handleCDSLocationChange(
305
+ cdsMin,
306
+ startCodonGenomicLocation,
307
+ feature,
308
+ true,
309
+ )
310
+ }
311
+ }}
312
+ >
313
+ {protein}
314
+ </Typography>,
315
+ )
316
+ } else if (['TAA', 'TAG', 'TGA'].includes(codonSeq)) {
317
+ elements.push(
318
+ <Typography
319
+ style={{ backgroundColor: 'red', color: 'white' }}
320
+ component={'span'}
321
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
322
+ key={codonGenomicPos}
323
+ >
324
+ {protein}
325
+ </Typography>,
326
+ )
327
+ } else {
328
+ elements.push(
329
+ // Pass the codonGenomicPos as the key to maintain the genomic position of the codon
330
+ <Typography component={'span'} key={codonGenomicPos}>
331
+ {protein}
332
+ </Typography>,
333
+ )
334
+ }
335
+ }
336
+ return elements
337
+ }
338
+
339
+ // Codon position is the index of the start codon in the CDS genomic sequence
340
+ // Calculate the genomic location of the start codon based on the codon position in the CDS
341
+ const getStartCodonGenomicLocation = (codonGenomicPosition: number) => {
342
+ const [firstLocation] = cdsLocations
343
+ 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
+ }
357
+
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)
366
+ }
367
+ cdsLen += locLength
368
+ }
369
+ return cdsMax
370
+ }
371
+
372
+ const trimTranslationSequence = () => {
373
+ const sequenceElements = getTranslationSequence()
374
+ const translationSequence = sequenceElements
375
+ .map((el) => el.props.children)
376
+ .join('')
377
+
378
+ if (
379
+ translationSequence.startsWith('M') &&
380
+ translationSequence.endsWith('*')
381
+ ) {
382
+ return
383
+ }
384
+
385
+ // NOTE: We are maintaining the genomic location of the codon start as the "key"
386
+ // in typography elements. See getTranslationSequence function
387
+ const translSeqCodonStartGenomicPosArr = []
388
+ for (const el of sequenceElements) {
389
+ translSeqCodonStartGenomicPosArr.push({
390
+ codonGenomicPos: el.key,
391
+ sequenceLetter: el.props.children,
392
+ })
393
+ }
394
+
395
+ if (translSeqCodonStartGenomicPosArr.length === 0) {
396
+ return
397
+ }
398
+
399
+ // Trim any sequence before first start codon and after last stop codon
400
+ const startCodonIndex = translationSequence.indexOf('M')
401
+ const stopCodonIndex = translationSequence.lastIndexOf('*') + 1
402
+ const startCodonPos =
403
+ translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
404
+ const stopCodonPos =
405
+ translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos
406
+
407
+ if (!startCodonPos || !stopCodonPos) {
408
+ return
409
+ }
410
+
411
+ const startCodonGenomicLoc = getStartCodonGenomicLocation(
412
+ startCodonPos as unknown as number,
413
+ )
414
+ const stopCodonGenomicLoc = getStopCodonGenomicLocation(
415
+ stopCodonPos as unknown as number,
416
+ )
417
+
418
+ if (startCodonGenomicLoc !== cdsMin) {
419
+ handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true)
420
+ }
421
+
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)
428
+ }
429
+ }
430
+
431
+ const copyToClipboard = () => {
432
+ const seqDiv = seqRef.current
433
+ if (!seqDiv) {
434
+ return
435
+ }
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])
443
+ }
444
+
445
+ return (
446
+ <div>
447
+ {cdsPresent && (
448
+ <div>
449
+ <Accordion defaultExpanded>
450
+ <StyledAccordionSummary
451
+ expandIcon={<ExpandMoreIcon style={{ color: 'white' }} />}
452
+ aria-controls="panel1-content"
453
+ id="panel1-header"
454
+ >
455
+ <Typography component="span" fontWeight={'bold'}>
456
+ Translation
457
+ </Typography>
458
+ </StyledAccordionSummary>
459
+ <AccordionDetails>
460
+ <SequenceContainer>
461
+ <Typography component={'span'} ref={seqRef}>
462
+ {getTranslationSequence()}
463
+ </Typography>
464
+ </SequenceContainer>
465
+ <div
466
+ style={{
467
+ marginTop: 10,
468
+ display: 'flex',
469
+ flexDirection: 'row',
470
+ alignItems: 'center',
471
+ gap: 10,
472
+ }}
473
+ >
474
+ <Tooltip title="Copy">
475
+ <ContentCopyIcon
476
+ style={{ fontSize: 15, cursor: 'pointer' }}
477
+ onClick={copyToClipboard}
478
+ />
479
+ </Tooltip>
480
+ <Tooltip title="Trim">
481
+ <ContentCutIcon
482
+ style={{ fontSize: 15, cursor: 'pointer' }}
483
+ onClick={trimTranslationSequence}
484
+ />
485
+ </Tooltip>
486
+ </div>
487
+ </AccordionDetails>
488
+ </Accordion>
489
+ <Grid2
490
+ container
491
+ justifyContent="center"
492
+ alignItems="center"
493
+ style={{ textAlign: 'center', marginTop: 10 }}
494
+ >
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}>
507
+ <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>
521
+ </div>
522
+ )}
523
+ <div style={{ marginTop: 5 }}>
524
+ {transcriptExonParts.map((loc, index) => {
525
+ return (
526
+ <div key={index}>
527
+ {loc.type === 'exon' && (
528
+ <Grid2
529
+ container
530
+ justifyContent="center"
531
+ alignItems="center"
532
+ style={{ textAlign: 'center' }}
533
+ >
534
+ <Grid2 size={1}>
535
+ {index !== 0 &&
536
+ getFivePrimeSpliceSite(loc, index).map((site, idx) => (
537
+ <Typography
538
+ key={idx}
539
+ component={'span'}
540
+ color={site.color}
541
+ >
542
+ {site.spliceSite}
543
+ </Typography>
544
+ ))}
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}>
562
+ <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}>
580
+ {index !== transcriptExonParts.length - 1 &&
581
+ getThreePrimeSpliceSite(loc, index).map((site, idx) => (
582
+ <Typography
583
+ key={idx}
584
+ component={'span'}
585
+ color={site.color}
586
+ >
587
+ {site.spliceSite}
588
+ </Typography>
589
+ ))}
590
+ </Grid2>
591
+ </Grid2>
592
+ )}
593
+ </div>
594
+ )
595
+ })}
596
+ </div>
597
+ </div>
598
+ )
599
+ },
600
+ )
@@ -0,0 +1,54 @@
1
+ import React from 'react'
2
+
3
+ import { AnnotationFeature } from '@apollo-annotation/mst'
4
+ import { observer } from 'mobx-react'
5
+ import { getFeatureId, getFeatureName, getStrand } from '../util'
6
+ import { Table, TableBody, TableCell, TableRow } from '@mui/material'
7
+ import styled from '@emotion/styled'
8
+
9
+ const HeaderTableCell = styled(TableCell)(() => ({
10
+ fontWeight: 'bold',
11
+ }))
12
+
13
+ export const TranscriptWidgetSummary = observer(
14
+ function TranscriptWidgetSummary(props: {
15
+ feature: AnnotationFeature
16
+ refName: string
17
+ }) {
18
+ const { feature } = props
19
+ const name = getFeatureName(feature)
20
+ const id = getFeatureId(feature)
21
+
22
+ return (
23
+ <Table
24
+ size="small"
25
+ sx={{ fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } }}
26
+ >
27
+ <TableBody>
28
+ {name !== '' && (
29
+ <TableRow>
30
+ <HeaderTableCell>Name</HeaderTableCell>
31
+ <TableCell>{getFeatureName(feature)}</TableCell>
32
+ </TableRow>
33
+ )}
34
+ {id !== '' && (
35
+ <TableRow>
36
+ <HeaderTableCell>ID</HeaderTableCell>
37
+ <TableCell>{getFeatureId(feature)}</TableCell>
38
+ </TableRow>
39
+ )}
40
+ <TableRow>
41
+ <HeaderTableCell>Location</HeaderTableCell>
42
+ <TableCell>
43
+ {props.refName}:{feature.min}..{feature.max}
44
+ </TableCell>
45
+ </TableRow>
46
+ <TableRow>
47
+ <HeaderTableCell>Strand</HeaderTableCell>
48
+ <TableCell>{getStrand(feature.strand)}</TableCell>
49
+ </TableRow>
50
+ </TableBody>
51
+ </Table>
52
+ )
53
+ },
54
+ )
@@ -61,6 +61,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
61
61
  const { model } = props
62
62
  const {
63
63
  loading,
64
+ apolloDragging,
64
65
  apolloRowHeight,
65
66
  contextMenuItems: getContextMenuItems,
66
67
  cursor,
@@ -218,18 +219,24 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
218
219
  if (!feature) {
219
220
  return null
220
221
  }
221
- const { topLevelFeature } = feature
222
- const row = parent
223
- ? model.getFeatureLayoutPosition(topLevelFeature)
224
- ?.layoutRow ?? 0
225
- : 0
222
+ let row = 0
223
+ const featureLayout = model.getFeatureLayoutPosition(feature)
224
+ if (featureLayout) {
225
+ row = featureLayout.layoutRow + featureLayout.featureRow
226
+ }
226
227
  const top = row * apolloRowHeight
227
228
  const height = apolloRowHeight
228
229
  return (
229
230
  <Tooltip key={checkResult._id} title={checkResult.message}>
230
231
  <Avatar
231
232
  className={classes.avatar}
232
- style={{ top, left, height, width: height }}
233
+ style={{
234
+ top,
235
+ left,
236
+ height,
237
+ width: height,
238
+ pointerEvents: apolloDragging ? 'none' : 'auto',
239
+ }}
233
240
  >
234
241
  <ErrorIcon />
235
242
  </Avatar>