@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
@@ -0,0 +1,193 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
2
+
3
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
4
+ import { MergeExonsChange } from '@apollo-annotation/shared'
5
+ import {
6
+ Box,
7
+ Button,
8
+ DialogActions,
9
+ DialogContent,
10
+ DialogContentText,
11
+ FormControl,
12
+ FormControlLabel,
13
+ Radio,
14
+ RadioGroup,
15
+ type SelectChangeEvent,
16
+ } from '@mui/material'
17
+ import { getSnapshot } from 'mobx-state-tree'
18
+ import React, { useState } from 'react'
19
+
20
+ import { type ChangeManager } from '../ChangeManager'
21
+ import { type ApolloSessionModel } from '../session'
22
+
23
+ import { Dialog } from './Dialog'
24
+
25
+ interface MergeExonsProps {
26
+ session: ApolloSessionModel
27
+ handleClose(): void
28
+ sourceFeature: AnnotationFeature
29
+ sourceAssemblyId: string
30
+ changeManager: ChangeManager
31
+ selectedFeature?: AnnotationFeature
32
+ setSelectedFeature(feature?: AnnotationFeature): void
33
+ }
34
+
35
+ function getNeighboringExons(
36
+ referenceExon: AnnotationFeature,
37
+ ): Record<string, AnnotationFeature> {
38
+ const neighboringExons: Record<string, AnnotationFeature> = {}
39
+ const tx = referenceExon.parent
40
+ if (!tx) {
41
+ throw new Error('Unable to find parent of reference exon')
42
+ }
43
+ let exons: AnnotationFeature[] = []
44
+ if (tx.children) {
45
+ for (const [, feature] of tx.children) {
46
+ if (feature.type === 'exon') {
47
+ exons.push(feature)
48
+ }
49
+ }
50
+ }
51
+ exons = exons.sort((a, b) => {
52
+ if (a.min === b.min) {
53
+ return a.max - b.max
54
+ }
55
+ return a.min - b.min
56
+ })
57
+ if (tx.strand && tx.strand === -1) {
58
+ exons = exons.reverse()
59
+ }
60
+ let i = 0
61
+ for (const x of exons) {
62
+ if (x._id === referenceExon._id) {
63
+ if (exons.length > i + 1) {
64
+ neighboringExons.three_prime = exons[i + 1]
65
+ }
66
+ if (i > 0) {
67
+ neighboringExons.five_prime = exons[i - 1]
68
+ }
69
+ break
70
+ }
71
+ i++
72
+ }
73
+ return neighboringExons
74
+ }
75
+
76
+ function makeRadioButtonName(
77
+ key: string,
78
+ neighboringExons: Record<string, AnnotationFeature>,
79
+ ): string {
80
+ const neighboringExon = neighboringExons[key]
81
+ let name
82
+ if (key === 'three_prime') {
83
+ name = `3'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`
84
+ } else if (key === 'five_prime') {
85
+ name = `5'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`
86
+ } else {
87
+ throw new Error(`Unexpected direction: "${key}"`)
88
+ }
89
+ return name
90
+ }
91
+
92
+ export function MergeExons({
93
+ changeManager,
94
+ handleClose,
95
+ selectedFeature,
96
+ setSelectedFeature,
97
+ sourceAssemblyId,
98
+ sourceFeature,
99
+ }: MergeExonsProps) {
100
+ const [errorMessage, setErrorMessage] = useState('')
101
+ const [selectedExon, setSelectedExon] = useState<AnnotationFeature>()
102
+
103
+ function onSubmit(event: React.FormEvent<HTMLFormElement>) {
104
+ event.preventDefault()
105
+ setErrorMessage('')
106
+ const { parent } = sourceFeature
107
+ if (!(selectedExon && parent)) {
108
+ return
109
+ }
110
+ if (selectedFeature?._id === sourceFeature._id) {
111
+ setSelectedFeature()
112
+ }
113
+ const change = new MergeExonsChange({
114
+ changedIds: [sourceFeature._id],
115
+ typeName: 'MergeExonsChange',
116
+ assembly: sourceAssemblyId,
117
+ firstExon: getSnapshot(sourceFeature),
118
+ secondExon: getSnapshot(selectedExon),
119
+ parentFeatureId: parent._id,
120
+ })
121
+ void changeManager.submit(change)
122
+ handleClose()
123
+ event.preventDefault()
124
+ }
125
+
126
+ const handleTypeChange = (e: SelectChangeEvent) => {
127
+ setErrorMessage('')
128
+ const { value } = e.target
129
+ setSelectedExon(neighboringExons[value])
130
+ }
131
+
132
+ const neighboringExons = getNeighboringExons(sourceFeature)
133
+
134
+ return (
135
+ <Dialog
136
+ open
137
+ title="Merge exons"
138
+ handleClose={handleClose}
139
+ maxWidth={false}
140
+ data-testid="merge-exons"
141
+ >
142
+ <form onSubmit={onSubmit}>
143
+ <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
144
+ {Object.keys(neighboringExons).length === 0
145
+ ? 'There are no neighbouring exons to merge with'
146
+ : 'Merge with exon on:'}
147
+ <FormControl style={{ marginTop: 5 }}>
148
+ <RadioGroup
149
+ aria-labelledby="demo-radio-buttons-group-label"
150
+ name="radio-buttons-group"
151
+ value={selectedExon}
152
+ onChange={handleTypeChange}
153
+ >
154
+ {Object.keys(neighboringExons).map((key) => (
155
+ <FormControlLabel
156
+ value={key}
157
+ key={key}
158
+ control={<Radio />}
159
+ label={
160
+ <Box display="flex" alignItems="center">
161
+ {makeRadioButtonName(key, neighboringExons)}
162
+ </Box>
163
+ }
164
+ />
165
+ ))}
166
+ </RadioGroup>
167
+ </FormControl>
168
+ </DialogContent>
169
+
170
+ <DialogActions>
171
+ <Button
172
+ variant="contained"
173
+ type="submit"
174
+ disabled={
175
+ Object.keys(neighboringExons).length === 0 ||
176
+ selectedExon === undefined
177
+ }
178
+ >
179
+ Submit
180
+ </Button>
181
+ <Button variant="outlined" type="submit" onClick={handleClose}>
182
+ Cancel
183
+ </Button>
184
+ </DialogActions>
185
+ </form>
186
+ {errorMessage ? (
187
+ <DialogContent>
188
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
189
+ </DialogContent>
190
+ ) : null}
191
+ </Dialog>
192
+ )
193
+ }
@@ -0,0 +1,182 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
2
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
3
+ import { MergeTranscriptsChange } from '@apollo-annotation/shared'
4
+ import {
5
+ Box,
6
+ Button,
7
+ DialogActions,
8
+ DialogContent,
9
+ DialogContentText,
10
+ FormControl,
11
+ FormControlLabel,
12
+ Radio,
13
+ RadioGroup,
14
+ type SelectChangeEvent,
15
+ } from '@mui/material'
16
+ import { getSnapshot } from 'mobx-state-tree'
17
+ import React, { useState } from 'react'
18
+
19
+ import { type ChangeManager } from '../ChangeManager'
20
+ import { type ApolloSessionModel } from '../session'
21
+
22
+ import { Dialog } from './Dialog'
23
+
24
+ interface MergeTranscriptsProps {
25
+ session: ApolloSessionModel
26
+ handleClose(): void
27
+ sourceFeature: AnnotationFeature
28
+ sourceAssemblyId: string
29
+ changeManager: ChangeManager
30
+ selectedFeature?: AnnotationFeature
31
+ setSelectedFeature(feature?: AnnotationFeature): void
32
+ }
33
+
34
+ function getTranscripts(
35
+ referenceTranscript: AnnotationFeature,
36
+ session: ApolloSessionModel,
37
+ ): Record<string, AnnotationFeature> {
38
+ const gene = referenceTranscript.parent
39
+ if (!gene) {
40
+ throw new Error('Unable to find parent of reference transcript')
41
+ }
42
+
43
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
44
+ if (!featureTypeOntology) {
45
+ throw new Error('featureTypeOntology is undefined')
46
+ }
47
+
48
+ const transcripts: Record<string, AnnotationFeature> = {}
49
+ if (gene.children) {
50
+ for (const [, feature] of gene.children) {
51
+ if (
52
+ featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
53
+ feature._id !== referenceTranscript._id
54
+ ) {
55
+ transcripts[feature._id] = feature
56
+ }
57
+ }
58
+ }
59
+ return transcripts
60
+ }
61
+
62
+ function makeRadioButtonName(transcript: AnnotationFeature): string {
63
+ let id
64
+ if (transcript.attributes.get('gff_name')) {
65
+ id = transcript.attributes.get('gff_name')?.join(',')
66
+ } else if (transcript.attributes.get('gff_id')) {
67
+ id = transcript.attributes.get('gff_id')?.join(',')
68
+ } else {
69
+ id = transcript._id
70
+ }
71
+ return `${id} [${transcript.min + 1}-${transcript.max}]`
72
+ }
73
+
74
+ export function MergeTranscripts({
75
+ changeManager,
76
+ handleClose,
77
+ selectedFeature,
78
+ session,
79
+ setSelectedFeature,
80
+ sourceAssemblyId,
81
+ sourceFeature,
82
+ }: MergeTranscriptsProps) {
83
+ const [errorMessage, setErrorMessage] = useState('')
84
+ const transcripts = getTranscripts(sourceFeature, session)
85
+ const firstTranscript = Object.keys(transcripts).at(0)
86
+ const [selectedTranscriptId, setSelectedTranscriptId] = useState<
87
+ string | undefined
88
+ >(firstTranscript)
89
+
90
+ function onSubmit(event: React.FormEvent<HTMLFormElement>) {
91
+ event.preventDefault()
92
+ setErrorMessage('')
93
+ if (!selectedTranscriptId) {
94
+ return
95
+ }
96
+ const selectedTranscript = transcripts[selectedTranscriptId]
97
+ if (selectedFeature?._id === sourceFeature._id) {
98
+ setSelectedFeature()
99
+ }
100
+
101
+ if (!sourceFeature.parent) {
102
+ throw new Error('Cannot find parent')
103
+ }
104
+
105
+ const change = new MergeTranscriptsChange({
106
+ changedIds: [sourceFeature._id],
107
+ typeName: 'MergeTranscriptsChange',
108
+ assembly: sourceAssemblyId,
109
+ firstTranscript: getSnapshot(sourceFeature),
110
+ secondTranscript: getSnapshot(selectedTranscript),
111
+ parentFeatureId: sourceFeature.parent._id,
112
+ })
113
+ void changeManager.submit(change)
114
+ handleClose()
115
+ }
116
+
117
+ const handleTypeChange = (e: SelectChangeEvent) => {
118
+ setErrorMessage('')
119
+ const { value } = e.target
120
+ setSelectedTranscriptId(value)
121
+ }
122
+
123
+ return (
124
+ <Dialog
125
+ open
126
+ title="Merge transcripts"
127
+ handleClose={handleClose}
128
+ maxWidth={false}
129
+ data-testid="merge-transcripts"
130
+ >
131
+ <form onSubmit={onSubmit}>
132
+ <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
133
+ {Object.keys(transcripts).length === 0
134
+ ? 'There are no transcripts to merge with'
135
+ : 'Merge with transcript:'}
136
+ <FormControl style={{ marginTop: 5 }}>
137
+ <RadioGroup
138
+ aria-labelledby="demo-radio-buttons-group-label"
139
+ name="radio-buttons-group"
140
+ value={selectedTranscriptId}
141
+ onChange={handleTypeChange}
142
+ >
143
+ {Object.keys(transcripts).map((key) => (
144
+ <FormControlLabel
145
+ value={key}
146
+ key={key}
147
+ control={<Radio />}
148
+ label={
149
+ <Box display="flex" alignItems="center">
150
+ {makeRadioButtonName(transcripts[key])}
151
+ </Box>
152
+ }
153
+ />
154
+ ))}
155
+ </RadioGroup>
156
+ </FormControl>
157
+ </DialogContent>
158
+
159
+ <DialogActions>
160
+ <Button
161
+ variant="contained"
162
+ type="submit"
163
+ disabled={
164
+ Object.keys(transcripts).length === 0 ||
165
+ selectedTranscriptId === undefined
166
+ }
167
+ >
168
+ Submit
169
+ </Button>
170
+ <Button variant="outlined" type="submit" onClick={handleClose}>
171
+ Cancel
172
+ </Button>
173
+ </DialogActions>
174
+ </form>
175
+ {errorMessage ? (
176
+ <DialogContent>
177
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
178
+ </DialogContent>
179
+ ) : null}
180
+ </Dialog>
181
+ )
182
+ }
@@ -4,9 +4,9 @@
4
4
  import { isAbortException } from '@jbrowse/core/util/aborting'
5
5
  import {
6
6
  Autocomplete,
7
- type AutocompleteRenderGetTagProps,
7
+ type AutocompleteRenderValueGetItemProps,
8
8
  Chip,
9
- Grid2,
9
+ Grid,
10
10
  TextField,
11
11
  Tooltip,
12
12
  Typography,
@@ -49,14 +49,14 @@ interface TermValue {
49
49
  // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
50
50
 
51
51
  function TermTagWithTooltip({
52
- getTagProps,
52
+ getItemProps,
53
53
  index,
54
54
  ontology,
55
55
  termId,
56
56
  }: {
57
57
  termId: string
58
58
  index: number
59
- getTagProps: AutocompleteRenderGetTagProps
59
+ getItemProps: AutocompleteRenderValueGetItemProps<true>
60
60
  ontology: OntologyRecord
61
61
  }) {
62
62
  const manager = getParent<OntologyManager>(ontology, 2)
@@ -100,7 +100,7 @@ function TermTagWithTooltip({
100
100
  label={errorMessage || manager.applyPrefixes(termId)}
101
101
  color={errorMessage ? 'error' : 'default'}
102
102
  size="small"
103
- {...getTagProps({ index })}
103
+ {...getItemProps({ index })}
104
104
  />
105
105
  </div>
106
106
  </Tooltip>
@@ -270,13 +270,13 @@ export function OntologyTermMultiSelect({
270
270
  inputValue={inputValue}
271
271
  />
272
272
  )}
273
- renderTags={(v, getTagProps) =>
273
+ renderValue={(v, getItemProps) =>
274
274
  v.map((option, index) => (
275
275
  <TermTagWithTooltip
276
276
  termId={option.term.id}
277
277
  index={index}
278
278
  ontology={ontology}
279
- getTagProps={getTagProps}
279
+ getItemProps={getItemProps}
280
280
  key={option.term.id}
281
281
  />
282
282
  ))
@@ -336,8 +336,8 @@ function Option(props: {
336
336
  // .join(', ')
337
337
  return (
338
338
  <li {...other}>
339
- <Grid2 container>
340
- <Grid2>
339
+ <Grid container>
340
+ <Grid>
341
341
  <Typography component="span">
342
342
  {ontologyManager.applyPrefixes(option.term.id)}
343
343
  </Typography>{' '}
@@ -347,8 +347,8 @@ function Option(props: {
347
347
  />{' '}
348
348
  {/* ({lblScore}) */}
349
349
  <dl>{fields}</dl>
350
- </Grid2>
351
- </Grid2>
350
+ </Grid>
351
+ </Grid>
352
352
  </li>
353
353
  )
354
354
  }
@@ -87,6 +87,13 @@ export function OpenLocalFile({ handleClose, session }: OpenLocalFileProps) {
87
87
  return
88
88
  }
89
89
 
90
+ const fileMetadata: { file?: string } = {}
91
+ if (isElectron) {
92
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
93
+ const { webUtils } = globalThis.require('electron')
94
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
95
+ fileMetadata.file = webUtils.getPathForFile(file) as string
96
+ }
90
97
  const assemblyConfig = {
91
98
  name: assemblyId,
92
99
  aliases: [assemblyName],
@@ -95,17 +102,14 @@ export function OpenLocalFile({ handleClose, session }: OpenLocalFileProps) {
95
102
  trackId: `sequenceConfigId-${assemblyName}`,
96
103
  type: 'ReferenceSequenceTrack',
97
104
  adapter: { type: 'ApolloSequenceAdapter', assemblyId },
98
- metadata: {
99
- apollo: true,
100
- ...(isElectron
101
- ? { file: (file as File & { path: string }).path }
102
- : {}),
103
- },
105
+ metadata: { apollo: true, ...fileMetadata },
104
106
  },
105
107
  }
106
108
 
107
109
  // Save assembly into session
108
- await (addSessionAssembly || addAssembly)(assemblyConfig)
110
+ await (isElectron
111
+ ? addAssembly?.(assemblyConfig)
112
+ : (addSessionAssembly || addAssembly)(assemblyConfig))
109
113
  const a = await assemblyManager.waitForAssembly(assemblyConfig.name)
110
114
  if (a) {
111
115
  // @ts-expect-error MST type coercion problem?
@@ -0,0 +1,134 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
2
+
3
+ import {
4
+ type AnnotationFeature,
5
+ type AnnotationFeatureSnapshot,
6
+ } from '@apollo-annotation/mst'
7
+ import { SplitExonChange } from '@apollo-annotation/shared'
8
+ import {
9
+ Button,
10
+ DialogActions,
11
+ DialogContent,
12
+ DialogContentText,
13
+ } from '@mui/material'
14
+ import ObjectID from 'bson-objectid'
15
+ import { getSnapshot } from 'mobx-state-tree'
16
+ import React, { useState } from 'react'
17
+
18
+ import { type ChangeManager } from '../ChangeManager'
19
+ import { type ApolloSessionModel } from '../session'
20
+
21
+ import { Dialog } from './Dialog'
22
+
23
+ interface SplitExonProps {
24
+ session: ApolloSessionModel
25
+ handleClose(): void
26
+ sourceFeature: AnnotationFeature
27
+ sourceAssemblyId: string
28
+ changeManager: ChangeManager
29
+ selectedFeature?: AnnotationFeature
30
+ setSelectedFeature(feature?: AnnotationFeature): void
31
+ }
32
+
33
+ interface splittableExon {
34
+ isSplittable: boolean
35
+ comment: string
36
+ }
37
+
38
+ function exonIsSplittable(
39
+ exonToBeSplit: AnnotationFeatureSnapshot,
40
+ ): splittableExon {
41
+ if (exonToBeSplit.max - exonToBeSplit.min < 2) {
42
+ return {
43
+ isSplittable: false,
44
+ comment: 'This exon is too short to be split',
45
+ }
46
+ }
47
+ return { isSplittable: true, comment: '' }
48
+ }
49
+
50
+ function makeDialogText(splitExon: AnnotationFeatureSnapshot): string {
51
+ const splittable = exonIsSplittable(splitExon)
52
+ if (splittable.isSplittable) {
53
+ return 'Are you sure you want to split the selected exon?'
54
+ }
55
+ return splittable.comment
56
+ }
57
+
58
+ export function SplitExon({
59
+ changeManager,
60
+ handleClose,
61
+ selectedFeature,
62
+ setSelectedFeature,
63
+ sourceAssemblyId,
64
+ sourceFeature,
65
+ }: SplitExonProps) {
66
+ const [errorMessage, setErrorMessage] = useState('')
67
+
68
+ const exonToBeSplit = getSnapshot(sourceFeature)
69
+
70
+ function onSubmit(event: React.FormEvent<HTMLFormElement>) {
71
+ event.preventDefault()
72
+ setErrorMessage('')
73
+ if (selectedFeature?._id === sourceFeature._id) {
74
+ setSelectedFeature()
75
+ }
76
+
77
+ const midpoint =
78
+ exonToBeSplit.min + (exonToBeSplit.max - exonToBeSplit.min) / 2
79
+ const upstreamCut = Math.floor(midpoint)
80
+ const downstreamCut = Math.ceil(midpoint)
81
+
82
+ if (!sourceFeature.parent?._id) {
83
+ throw new Error('Splitting an exon without parent is not possible yet')
84
+ }
85
+
86
+ const change = new SplitExonChange({
87
+ changedIds: [sourceFeature._id],
88
+ typeName: 'SplitExonChange',
89
+ assembly: sourceAssemblyId,
90
+ exonToBeSplit,
91
+ parentFeatureId: sourceFeature.parent._id,
92
+ upstreamCut,
93
+ downstreamCut,
94
+ leftExonId: new ObjectID().toHexString(),
95
+ rightExonId: new ObjectID().toHexString(),
96
+ })
97
+ void changeManager.submit(change)
98
+ handleClose()
99
+ event.preventDefault()
100
+ }
101
+
102
+ return (
103
+ <Dialog
104
+ open
105
+ title="Split exon"
106
+ handleClose={handleClose}
107
+ maxWidth={false}
108
+ data-testid="split-exon"
109
+ >
110
+ <form onSubmit={onSubmit}>
111
+ <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
112
+ <DialogContentText>{makeDialogText(exonToBeSplit)}</DialogContentText>
113
+ </DialogContent>
114
+ <DialogActions>
115
+ <Button
116
+ variant="contained"
117
+ type="submit"
118
+ disabled={!exonIsSplittable(exonToBeSplit).isSplittable}
119
+ >
120
+ Yes
121
+ </Button>
122
+ <Button variant="outlined" type="submit" onClick={handleClose}>
123
+ Cancel
124
+ </Button>
125
+ </DialogActions>
126
+ </form>
127
+ {errorMessage ? (
128
+ <DialogContent>
129
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
130
+ </DialogContent>
131
+ ) : null}
132
+ </Dialog>
133
+ )
134
+ }
@@ -127,7 +127,7 @@ export function ViewCheckResults({
127
127
  >
128
128
  {assemblies.map((option) => (
129
129
  <MenuItem key={option.name} value={option.name}>
130
- {option.displayName ?? option.name}
130
+ {option.displayName}
131
131
  </MenuItem>
132
132
  ))}
133
133
  </Select>
@@ -1,4 +1,5 @@
1
1
  export * from './AddAssembly'
2
+ export * from './AddAssemblyAliases'
2
3
  export * from './AddChildFeature'
3
4
  export * from './AddFeature'
4
5
  export * from './CopyFeature'
@@ -9,7 +10,10 @@ export * from './ImportFeatures'
9
10
  export * from './LogOut'
10
11
  export * from './ManageChecks'
11
12
  export * from './ManageUsers'
13
+ export * from './MergeExons'
14
+ export * from './MergeTranscripts'
12
15
  export * from './OpenLocalFile'
13
16
  export * from './ViewChangeLog'
14
17
  export * from './AddRefSeqAliases'
15
18
  export * from './ViewCheckResults'
19
+ export * from './SplitExon'
package/src/config.ts CHANGED
@@ -10,6 +10,17 @@ const ApolloPluginConfigurationSchema = ConfigurationSchema('ApolloPlugin', {
10
10
  type: 'string',
11
11
  defaultValue: 'Sequence Ontology',
12
12
  },
13
+ hasRole: {
14
+ description: 'Flag used internally by jbrowse-plugin-apollo',
15
+ type: 'boolean',
16
+ defaultValue: false,
17
+ },
18
+ backgroundColorForFeature: {
19
+ description: 'Color ',
20
+ type: 'string',
21
+ defaultValue: 'jexl:colorFeature(featureType)',
22
+ contextVariable: ['featureType'],
23
+ },
13
24
  })
14
25
 
15
26
  export default ApolloPluginConfigurationSchema
@@ -137,6 +137,7 @@ export function annotationFromJBrowseFeature(
137
137
  contextMenuItems() {
138
138
  const session = getSession(self)
139
139
  const assembly = self.getAssembly()
140
+ const region = self.getFirstRegion()
140
141
  const feature = self.contextMenuFeature
141
142
  if (!feature) {
142
143
  return superContextMenuItems()
@@ -158,6 +159,7 @@ export function annotationFromJBrowseFeature(
158
159
  annotationFeature: self.getAnnotationFeature(assembly),
159
160
  assembly,
160
161
  refSeqId: self.getRefSeqId(assembly),
162
+ region,
161
163
  },
162
164
  ],
163
165
  )