@apollo-annotation/jbrowse-plugin-apollo 0.1.21 → 0.2.1

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 (57) hide show
  1. package/dist/index.esm.js +431 -570
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +439 -578
  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 +11064 -1091
  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 -5
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +4 -2
  13. package/src/ApolloInternetAccount/configSchema.ts +1 -1
  14. package/src/ApolloInternetAccount/model.ts +5 -10
  15. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +1 -1
  16. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +4 -5
  17. package/src/BackendDrivers/DesktopFileDriver.ts +3 -2
  18. package/src/FeatureDetailsWidget/Attributes.tsx +1 -6
  19. package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -0
  20. package/src/FeatureDetailsWidget/StringTextField.tsx +1 -0
  21. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +131 -382
  22. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +209 -284
  23. package/src/FeatureDetailsWidget/model.ts +4 -4
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -0
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +25 -3
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +95 -32
  27. package/src/LinearApolloDisplay/stateModel/base.ts +5 -3
  28. package/src/LinearApolloDisplay/stateModel/index.ts +1 -1
  29. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +1 -1
  30. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -1
  31. package/src/OntologyManager/OntologyStore/fulltext.ts +5 -2
  32. package/src/OntologyManager/OntologyStore/index.ts +25 -22
  33. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +8 -3
  34. package/src/OntologyManager/index.ts +31 -8
  35. package/src/SixFrameFeatureDisplay/stateModel.ts +1 -1
  36. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +1 -0
  37. package/src/TabularEditor/HybridGrid/NumberCell.tsx +1 -0
  38. package/src/TabularEditor/model.ts +1 -1
  39. package/src/components/AddChildFeature.tsx +1 -0
  40. package/src/components/AddFeature.tsx +1 -1
  41. package/src/components/AddRefSeqAliases.tsx +1 -0
  42. package/src/components/CopyFeature.tsx +1 -0
  43. package/src/components/DeleteAssembly.tsx +1 -0
  44. package/src/components/DeleteFeature.tsx +1 -0
  45. package/src/components/LogOut.tsx +2 -1
  46. package/src/components/ManageChecks.tsx +1 -1
  47. package/src/components/ManageUsers.tsx +2 -1
  48. package/src/components/OntologyTermAutocomplete.tsx +7 -9
  49. package/src/components/OntologyTermMultiSelect.tsx +2 -1
  50. package/src/components/OpenLocalFile.tsx +3 -1
  51. package/src/components/ViewChangeLog.tsx +1 -0
  52. package/src/components/ViewCheckResults.tsx +1 -0
  53. package/src/config.ts +5 -0
  54. package/src/extensions/annotationFromPileup.ts +1 -1
  55. package/src/makeDisplayComponent.tsx +28 -7
  56. package/src/session/ClientDataStore.ts +1 -1
  57. package/src/session/session.ts +2 -1
@@ -1,131 +1,149 @@
1
- import { AnnotationFeature, ApolloRefSeqI } from '@apollo-annotation/mst'
1
+ import { AnnotationFeature } from '@apollo-annotation/mst'
2
2
  import { splitStringIntoChunks } from '@apollo-annotation/shared'
3
3
  import { revcom } from '@jbrowse/core/util'
4
4
  import {
5
5
  Button,
6
6
  MenuItem,
7
+ Paper,
7
8
  Select,
8
9
  SelectChangeEvent,
9
10
  Typography,
11
+ useTheme,
10
12
  } from '@mui/material'
11
13
  import { observer } from 'mobx-react'
12
- import React, { useState } from 'react'
14
+ import React, { useRef, useState } from 'react'
13
15
 
14
16
  import { ApolloSessionModel } from '../session'
15
17
 
16
- export interface CDSInfo {
17
- id: string
18
- type: string
19
- strand: number
20
- min: number
21
- oldMin: number
22
- max: number
23
- oldMax: number
24
- startSeq: string
25
- endSeq: string
18
+ const SEQUENCE_WRAP_LENGTH = 60
19
+
20
+ type SegmentType = 'upOrDownstream' | 'UTR' | 'CDS' | 'intron' | 'protein'
21
+ type SegmentListType = 'CDS' | 'cDNA' | 'genomic'
22
+
23
+ interface SequenceSegment {
24
+ type: SegmentType
25
+ sequenceLines: string[]
26
+ locs: { min: number; max: number }[]
26
27
  }
27
28
 
28
- const getCDSInfo = (
29
+ function getSequenceSegments(
30
+ segmentType: SegmentListType,
29
31
  feature: AnnotationFeature,
30
- refData: ApolloRefSeqI,
31
- ): CDSInfo[] => {
32
- const CDSresult: CDSInfo[] = []
33
- const traverse = (
34
- currentFeature: AnnotationFeature,
35
- isParentMRNA: boolean,
36
- ) => {
37
- if (
38
- isParentMRNA &&
39
- (currentFeature.type === 'CDS' ||
40
- currentFeature.type === 'three_prime_UTR' ||
41
- currentFeature.type === 'five_prime_UTR')
42
- ) {
43
- let startSeq = refData.getSequence(
44
- Number(currentFeature.min) - 2,
45
- Number(currentFeature.min),
46
- )
47
- let endSeq = refData.getSequence(
48
- Number(currentFeature.max),
49
- Number(currentFeature.max) + 2,
50
- )
51
-
52
- if (currentFeature.strand === -1 && startSeq && endSeq) {
53
- startSeq = revcom(startSeq)
54
- endSeq = revcom(endSeq)
55
- }
56
- const oneCDS: CDSInfo = {
57
- id: currentFeature._id,
58
- type: currentFeature.type,
59
- strand: Number(currentFeature.strand),
60
- min: currentFeature.min + 1,
61
- max: currentFeature.max + 1,
62
- oldMin: currentFeature.min + 1,
63
- oldMax: currentFeature.max + 1,
64
- startSeq: startSeq || '',
65
- endSeq: endSeq || '',
32
+ getSequence: (min: number, max: number) => string,
33
+ ) {
34
+ const segments: SequenceSegment[] = []
35
+ const { cdsLocations, strand, transcriptParts } = feature
36
+ switch (segmentType) {
37
+ case 'genomic':
38
+ case 'cDNA': {
39
+ const [firstLocation] = transcriptParts
40
+ for (const loc of firstLocation) {
41
+ if (segmentType === 'cDNA' && loc.type === 'intron') {
42
+ continue
43
+ }
44
+ let sequence = getSequence(loc.min, loc.max)
45
+ if (strand === -1) {
46
+ sequence = revcom(sequence)
47
+ }
48
+ const type: SegmentType =
49
+ loc.type === 'fivePrimeUTR' || loc.type === 'threePrimeUTR'
50
+ ? 'UTR'
51
+ : loc.type
52
+ const previousSegment = segments.at(-1)
53
+ if (!previousSegment) {
54
+ const sequenceLines = splitStringIntoChunks(
55
+ sequence,
56
+ SEQUENCE_WRAP_LENGTH,
57
+ )
58
+ segments.push({
59
+ type,
60
+ sequenceLines,
61
+ locs: [{ min: loc.min, max: loc.max }],
62
+ })
63
+ continue
64
+ }
65
+ if (previousSegment.type === type) {
66
+ const [previousSegmentFirstLine, ...previousSegmentFollowingLines] =
67
+ previousSegment.sequenceLines
68
+ const newSequence = previousSegmentFollowingLines.join('') + sequence
69
+ previousSegment.sequenceLines = [
70
+ previousSegmentFirstLine,
71
+ ...splitStringIntoChunks(newSequence, SEQUENCE_WRAP_LENGTH),
72
+ ]
73
+ previousSegment.locs.push({ min: loc.min, max: loc.max })
74
+ } else {
75
+ const count = segments.reduce(
76
+ (accumulator, currentSegment) =>
77
+ accumulator +
78
+ currentSegment.sequenceLines.reduce(
79
+ (subAccumulator, currentLine) =>
80
+ subAccumulator + currentLine.length,
81
+ 0,
82
+ ),
83
+ 0,
84
+ )
85
+ const previousLineLength = count % SEQUENCE_WRAP_LENGTH
86
+ const newSegmentFirstLineLength =
87
+ SEQUENCE_WRAP_LENGTH - previousLineLength
88
+ const newSegmentFirstLine = sequence.slice(
89
+ 0,
90
+ newSegmentFirstLineLength,
91
+ )
92
+ const newSegmentRemainderLines = splitStringIntoChunks(
93
+ sequence.slice(newSegmentFirstLineLength),
94
+ SEQUENCE_WRAP_LENGTH,
95
+ )
96
+ segments.push({
97
+ type,
98
+ sequenceLines: [newSegmentFirstLine, ...newSegmentRemainderLines],
99
+ locs: [{ min: loc.min, max: loc.max }],
100
+ })
101
+ }
66
102
  }
67
- CDSresult.push(oneCDS)
103
+ return segments
68
104
  }
69
- if (currentFeature.children) {
70
- for (const child of currentFeature.children) {
71
- traverse(child[1], feature.type === 'mRNA')
105
+ case 'CDS': {
106
+ let wholeSequence = ''
107
+ const [firstLocation] = cdsLocations
108
+ const locs: { min: number; max: number }[] = []
109
+ for (const loc of firstLocation) {
110
+ let sequence = getSequence(loc.min, loc.max)
111
+ if (strand === -1) {
112
+ sequence = revcom(sequence)
113
+ }
114
+ wholeSequence += sequence
115
+ locs.push({ min: loc.min, max: loc.max })
72
116
  }
117
+ const sequenceLines = splitStringIntoChunks(
118
+ wholeSequence,
119
+ SEQUENCE_WRAP_LENGTH,
120
+ )
121
+ segments.push({ type: 'CDS', sequenceLines, locs })
122
+ return segments
73
123
  }
74
124
  }
75
- traverse(feature, feature.type === 'mRNA')
76
- CDSresult.sort((a, b) => {
77
- return Number(a.min) - Number(b.min)
78
- })
79
- if (CDSresult.length > 0) {
80
- CDSresult[0].startSeq = ''
81
-
82
- // eslint-disable-next-line unicorn/prefer-at
83
- CDSresult[CDSresult.length - 1].endSeq = ''
125
+ }
84
126
 
85
- // Loop through the array and clear "startSeq" or "endSeq" based on the conditions
86
- for (let i = 0; i < CDSresult.length; i++) {
87
- if (i > 0 && CDSresult[i].min === CDSresult[i - 1].max) {
88
- // Clear "startSeq" if the current item's "start" is equal to the previous item's "end"
89
- CDSresult[i].startSeq = ''
90
- }
91
- if (
92
- i < CDSresult.length - 1 &&
93
- CDSresult[i].max === CDSresult[i + 1].min
94
- ) {
95
- // Clear "endSeq" if the next item's "start" is equal to the current item's "end"
96
- CDSresult[i].endSeq = ''
97
- }
127
+ function getSegmentColor(type: SegmentType) {
128
+ switch (type) {
129
+ case 'upOrDownstream': {
130
+ return 'rgb(255,255,255)'
131
+ }
132
+ case 'UTR': {
133
+ return 'rgb(194,106,119)'
134
+ }
135
+ case 'CDS': {
136
+ return 'rgb(93,168,153)'
137
+ }
138
+ case 'intron': {
139
+ return 'rgb(187,187,187)'
140
+ }
141
+ case 'protein': {
142
+ return 'rgb(148,203,236)'
98
143
  }
99
144
  }
100
- return CDSresult
101
- }
102
-
103
- interface Props {
104
- textSegments: { text: string; color: string }[]
105
145
  }
106
146
 
107
- function formatSequence(
108
- seq: string,
109
- refName: string,
110
- start: number,
111
- end: number,
112
- wrap?: number,
113
- ) {
114
- const header = `>${refName}:${start + 1}–${end}\n`
115
- const body =
116
- wrap === undefined ? seq : splitStringIntoChunks(seq, wrap).join('\n')
117
- return `${header}${body}`
118
- }
119
-
120
- export const intronColor = 'rgb(120,120,120)' // Slightly brighter gray
121
- export const utrColor = 'rgb(20,200,200)' // Slightly brighter cyan
122
- export const proteinColor = 'rgb(220,70,220)' // Slightly brighter magenta
123
- export const cdsColor = 'rgb(240,200,20)' // Slightly brighter yellow
124
- export const updownstreamColor = 'rgb(255,130,130)' // Slightly brighter red
125
- export const genomeColor = 'rgb(20,230,20)' // Slightly brighter green
126
-
127
- let textSegments = [{ text: '', color: '' }]
128
-
129
147
  export const TranscriptSequence = observer(function TranscriptSequence({
130
148
  assembly,
131
149
  feature,
@@ -140,7 +158,9 @@ export const TranscriptSequence = observer(function TranscriptSequence({
140
158
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly)
141
159
  const refData = currentAssembly?.getByRefName(refName)
142
160
  const [showSequence, setShowSequence] = useState(false)
143
- const [selectedOption, setSelectedOption] = useState('Select')
161
+ const [selectedOption, setSelectedOption] = useState<SegmentListType>('CDS')
162
+ const theme = useTheme()
163
+ const seqRef = useRef<HTMLDivElement>(null)
144
164
 
145
165
  if (!(currentAssembly && refData)) {
146
166
  return null
@@ -149,215 +169,120 @@ export const TranscriptSequence = observer(function TranscriptSequence({
149
169
  if (!refSeq) {
150
170
  return null
151
171
  }
152
- const transcriptItems = getCDSInfo(feature, refData)
153
- const { max, min } = feature
154
- let sequence = ''
155
- if (showSequence) {
156
- getSequenceAsString(min, max)
157
- }
158
-
159
- function getSequenceAsString(start: number, end: number): string {
160
- sequence = refSeq?.getSequence(start, end) ?? ''
161
- if (sequence === '') {
162
- void session.apolloDataStore.loadRefSeq([
163
- { assemblyName: assembly, refName, start, end },
164
- ])
165
- } else {
166
- sequence = formatSequence(sequence, refName, start, end)
167
- }
168
- getSequenceAsTextSegment(selectedOption) // For color coded sequence
169
- return sequence
172
+ if (feature.type !== 'mRNA') {
173
+ return null
170
174
  }
171
175
 
172
176
  const handleSeqButtonClick = () => {
173
177
  setShowSequence(!showSequence)
174
178
  }
175
179
 
176
- function getSequenceAsTextSegment(option: string) {
177
- let seqData = ''
178
- textSegments = []
179
- if (!refData) {
180
- return
181
- }
182
- switch (option) {
183
- case 'CDS': {
184
- textSegments.push({ text: `>${refName} : CDS\n`, color: 'black' })
185
- for (const item of transcriptItems) {
186
- if (item.type === 'CDS') {
187
- const refSeq: string = refData.getSequence(
188
- Number(item.min + 1),
189
- Number(item.max),
190
- )
191
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq
192
- textSegments.push({ text: seqData, color: cdsColor })
193
- }
194
- }
195
- break
196
- }
197
- case 'cDNA': {
198
- textSegments.push({ text: `>${refName} : cDNA\n`, color: 'black' })
199
- for (const item of transcriptItems) {
200
- if (
201
- item.type === 'CDS' ||
202
- item.type === 'three_prime_UTR' ||
203
- item.type === 'five_prime_UTR'
204
- ) {
205
- const refSeq: string = refData.getSequence(
206
- Number(item.min + 1),
207
- Number(item.max),
208
- )
209
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq
210
- if (item.type === 'CDS') {
211
- textSegments.push({ text: seqData, color: cdsColor })
212
- } else {
213
- textSegments.push({ text: seqData, color: utrColor })
214
- }
215
- }
216
- }
217
- break
218
- }
219
- case 'Full': {
220
- textSegments.push({
221
- text: `>${refName} : Full genomic\n`,
222
- color: 'black',
223
- })
224
- let lastEnd = 0
225
- let count = 0
226
- for (const item of transcriptItems) {
227
- count++
228
- if (
229
- lastEnd != 0 &&
230
- lastEnd != Number(item.min) &&
231
- count != transcriptItems.length
232
- ) {
233
- // Intron etc. between CDS/UTRs. No need to check this on very last item
234
- const refSeq: string = refData.getSequence(
235
- lastEnd + 1,
236
- Number(item.min) - 1,
237
- )
238
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq
239
- textSegments.push({ text: seqData, color: 'black' })
240
- }
241
- if (
242
- item.type === 'CDS' ||
243
- item.type === 'three_prime_UTR' ||
244
- item.type === 'five_prime_UTR'
245
- ) {
246
- const refSeq: string = refData.getSequence(
247
- Number(item.min + 1),
248
- Number(item.max),
249
- )
250
- seqData += item.strand === -1 && refSeq ? revcom(refSeq) : refSeq
251
- switch (item.type) {
252
- case 'CDS': {
253
- textSegments.push({ text: seqData, color: cdsColor })
254
- break
255
- }
256
- case 'three_prime_UTR': {
257
- textSegments.push({ text: seqData, color: utrColor })
258
- break
259
- }
260
- case 'five_prime_UTR': {
261
- textSegments.push({ text: seqData, color: utrColor })
262
- break
263
- }
264
- default: {
265
- textSegments.push({ text: seqData, color: 'black' })
266
- break
267
- }
268
- }
269
- }
270
- lastEnd = Number(item.max)
271
- }
272
- break
273
- }
274
- }
275
- }
276
-
277
180
  function handleChangeSeqOption(e: SelectChangeEvent) {
278
181
  const option = e.target.value
279
- setSelectedOption(option)
280
- getSequenceAsTextSegment(option)
182
+ setSelectedOption(option as SegmentListType)
281
183
  }
282
184
 
283
185
  // Function to copy text to clipboard
284
186
  const copyToClipboard = () => {
285
- const textToCopy = textSegments.map((segment) => segment.text).join('')
286
-
287
- if (textToCopy) {
288
- navigator.clipboard
289
- .writeText(textToCopy)
290
- .then(() => {
291
- // console.log('Text copied to clipboard!')
292
- })
293
- .catch((error: unknown) => {
294
- console.error('Failed to copy text to clipboard', error)
295
- })
187
+ const seqDiv = seqRef.current
188
+ if (!seqDiv) {
189
+ return
296
190
  }
191
+ const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' })
192
+ const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' })
193
+ const clipboardItem = new ClipboardItem({
194
+ [textBlob.type]: textBlob,
195
+ [htmlBlob.type]: htmlBlob,
196
+ })
197
+ void navigator.clipboard.write([clipboardItem])
297
198
  }
298
199
 
299
- const ColoredText: React.FC<Props> = ({ textSegments }) => {
300
- return (
301
- <div>
302
- {textSegments.map((segment, index) => (
303
- <span key={index} style={{ color: segment.color }}>
304
- {splitStringIntoChunks(segment.text, 150).join('\n')}
305
- </span>
306
- ))}
307
- </div>
308
- )
200
+ const sequenceSegments = showSequence
201
+ ? getSequenceSegments(selectedOption, feature, (min: number, max: number) =>
202
+ refData.getSequence(min, max),
203
+ )
204
+ : []
205
+ const locationIntervals: { min: number; max: number }[] = []
206
+ if (showSequence) {
207
+ const allLocs = sequenceSegments.flatMap((segment) => segment.locs)
208
+ let [previous] = allLocs
209
+ for (let i = 1; i < allLocs.length; i++) {
210
+ if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
211
+ previous = {
212
+ min: Math.min(previous.min, allLocs[i].min),
213
+ max: Math.max(previous.max, allLocs[i].max),
214
+ }
215
+ } else {
216
+ locationIntervals.push(previous)
217
+ previous = allLocs[i]
218
+ }
219
+ }
220
+ locationIntervals.push(previous)
309
221
  }
310
222
 
311
223
  return (
312
224
  <>
313
- <Typography
314
- style={{ display: 'inline', marginLeft: '15px' }}
315
- variant="h5"
316
- >
317
- Sequence
318
- </Typography>
225
+ <Typography variant="h5">Sequence</Typography>
319
226
  <div>
320
- <Button
321
- variant="contained"
322
- style={{ marginLeft: '15px' }}
323
- onClick={handleSeqButtonClick}
324
- >
227
+ <Button variant="contained" onClick={handleSeqButtonClick}>
325
228
  {showSequence ? 'Hide sequence' : 'Show sequence'}
326
229
  </Button>
327
230
  </div>
328
- <div>
329
- {showSequence && (
231
+ {showSequence && (
232
+ <>
330
233
  <Select
234
+ defaultValue="CDS"
331
235
  value={selectedOption}
332
236
  onChange={handleChangeSeqOption}
333
- style={{ width: '150px', marginLeft: '15px', height: '25px' }}
334
237
  >
335
- <MenuItem value={'Select'}>Select</MenuItem>
336
- <MenuItem value={'CDS'}>CDS</MenuItem>
337
- <MenuItem value={'cDNA'}>cDNA</MenuItem>
338
- <MenuItem value={'Full'}>Full genomics</MenuItem>
238
+ <MenuItem value="CDS">CDS</MenuItem>
239
+ <MenuItem value="cDNA">cDNA</MenuItem>
240
+ <MenuItem value="genomic">Genomic</MenuItem>
339
241
  </Select>
340
- )}
341
- </div>
342
- <div
343
- style={{
344
- width: '500px',
345
- marginLeft: '15px',
346
- height: '300px',
347
- overflowY: 'auto',
348
- border: '1px solid #ccc',
349
- }}
350
- >
351
- {showSequence && <ColoredText textSegments={textSegments} />}
352
- </div>
353
- {showSequence && (
354
- <Button
355
- variant="contained"
356
- style={{ marginLeft: '15px' }}
357
- onClick={copyToClipboard}
358
- >
359
- Copy sequence
360
- </Button>
242
+ <Paper
243
+ style={{
244
+ fontFamily: 'monospace',
245
+ padding: theme.spacing(),
246
+ overflowX: 'auto',
247
+ }}
248
+ ref={seqRef}
249
+ >
250
+ &gt;{refSeq.name}:
251
+ {locationIntervals
252
+ .map((interval) =>
253
+ feature.strand === 1
254
+ ? `${interval.min + 1}-${interval.max}`
255
+ : `${interval.max}-${interval.min + 1}`,
256
+ )
257
+ .join(';')}
258
+ ({feature.strand === 1 ? '+' : '-'})
259
+ <br />
260
+ {sequenceSegments.map((segment, index) => (
261
+ <span
262
+ key={`${segment.type}-${index}`}
263
+ style={{
264
+ background: getSegmentColor(segment.type),
265
+ color: theme.palette.getContrastText(
266
+ getSegmentColor(segment.type),
267
+ ),
268
+ }}
269
+ >
270
+ {segment.sequenceLines.map((sequenceLine, idx) => (
271
+ <React.Fragment key={`${sequenceLine.slice(0, 5)}-${idx}`}>
272
+ {sequenceLine}
273
+ {idx === segment.sequenceLines.length - 1 &&
274
+ sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (
275
+ <br />
276
+ )}
277
+ </React.Fragment>
278
+ ))}
279
+ </span>
280
+ ))}
281
+ </Paper>
282
+ <Button variant="contained" onClick={copyToClipboard}>
283
+ Copy sequence
284
+ </Button>
285
+ </>
361
286
  )}
362
287
  </>
363
288
  )
@@ -65,10 +65,10 @@ export const ApolloFeatureDetailsWidgetModel = types
65
65
 
66
66
  // eslint disables because of
67
67
  // https://mobx-state-tree.js.org/tips/typescript#using-a-mst-type-at-design-time
68
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
68
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
69
69
  export interface ApolloFeatureDetailsWidget
70
70
  extends Instance<typeof ApolloFeatureDetailsWidgetModel> {}
71
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
71
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
72
72
  export interface ApolloFeatureDetailsWidgetSnapshot
73
73
  extends SnapshotIn<typeof ApolloFeatureDetailsWidgetModel> {}
74
74
 
@@ -126,9 +126,9 @@ export const ApolloTranscriptDetailsModel = types
126
126
 
127
127
  // eslint disables because of
128
128
  // https://mobx-state-tree.js.org/tips/typescript#using-a-mst-type-at-design-time
129
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
129
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
130
130
  export interface ApolloTranscriptDetailsWidget
131
131
  extends Instance<typeof ApolloTranscriptDetailsModel> {}
132
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
132
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
133
133
  export interface ApolloTranscriptDetailsWidgetSnapshot
134
134
  extends SnapshotIn<typeof ApolloTranscriptDetailsModel> {}
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
1
2
  /* eslint-disable @typescript-eslint/unbound-method */
2
3
  /* eslint-disable @typescript-eslint/no-misused-promises */
3
4
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
@@ -2,7 +2,11 @@ import { AnnotationFeature } from '@apollo-annotation/mst'
2
2
  import { Theme, alpha } from '@mui/material'
3
3
  import { MenuItem } from '@jbrowse/core/ui'
4
4
 
5
- import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util'
5
+ import {
6
+ AbstractSessionModel,
7
+ isSessionModelWithWidgets,
8
+ SessionWithWidgets,
9
+ } from '@jbrowse/core/util'
6
10
 
7
11
  import {
8
12
  AddChildFeature,
@@ -175,7 +179,7 @@ function drawTooltip(
175
179
  if (!position) {
176
180
  return
177
181
  }
178
- const { layoutIndex, layoutRow } = position
182
+ const { featureRow, layoutIndex, layoutRow } = position
179
183
  const { bpPerPx, displayedRegions, offsetPx } = lgv
180
184
  const displayedRegion = displayedRegions[layoutIndex]
181
185
  const { refName, reversed } = displayedRegion
@@ -191,7 +195,7 @@ function drawTooltip(
191
195
  coord: reversed ? max : min,
192
196
  regionNumber: layoutIndex,
193
197
  })?.offsetPx ?? 0) - offsetPx
194
- const top = layoutRow * apolloRowHeight
198
+ const top = (layoutRow + featureRow) * apolloRowHeight
195
199
  const widthPx = length / bpPerPx
196
200
 
197
201
  const featureType = `Type: ${feature.type}`
@@ -385,6 +389,24 @@ function getContextMenuItems(
385
389
  },
386
390
  },
387
391
  )
392
+ if (sourceFeature.type === 'mRNA' && isSessionModelWithWidgets(session)) {
393
+ menuItems.push({
394
+ label: 'Edit transcript details',
395
+ onClick: () => {
396
+ const apolloTranscriptWidget = session.addWidget(
397
+ 'ApolloTranscriptDetails',
398
+ 'apolloTranscriptDetails',
399
+ {
400
+ feature: sourceFeature,
401
+ assembly: currentAssemblyId,
402
+ changeManager,
403
+ refName: region.refName,
404
+ },
405
+ )
406
+ session.showWidget(apolloTranscriptWidget)
407
+ },
408
+ })
409
+ }
388
410
  return menuItems
389
411
  }
390
412