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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.esm.js +2679 -850
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +5194 -1258
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
  13. package/src/ChangeManager.ts +10 -6
  14. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  15. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
  17. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
  23. package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
  26. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
  27. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  28. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
  29. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
  30. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
  31. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
  33. package/src/OntologyManager/index.ts +4 -1
  34. package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
  35. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  36. package/src/components/AddAssemblyAliases.tsx +114 -0
  37. package/src/components/AddChildFeature.tsx +3 -6
  38. package/src/components/AddFeature.tsx +14 -15
  39. package/src/components/CopyFeature.tsx +2 -4
  40. package/src/components/CreateApolloAnnotation.tsx +334 -151
  41. package/src/components/DeleteFeature.tsx +358 -11
  42. package/src/components/DownloadGFF3.tsx +20 -1
  43. package/src/components/FilterTranscripts.tsx +86 -0
  44. package/src/components/MergeExons.tsx +193 -0
  45. package/src/components/MergeTranscripts.tsx +185 -0
  46. package/src/components/SplitExon.tsx +134 -0
  47. package/src/components/index.ts +3 -0
  48. package/src/config.ts +5 -0
  49. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  50. package/src/extensions/annotationFromPileup.ts +99 -89
  51. package/src/session/session.ts +26 -13
  52. package/src/util/annotationFeatureUtils.ts +65 -0
  53. package/src/util/copyToClipboard.ts +21 -0
  54. package/src/util/glyphUtils.ts +49 -0
  55. package/src/util/index.ts +2 -0
  56. package/src/util/mouseEventsUtils.ts +113 -0
@@ -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,185 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
2
+ /* eslint-disable @typescript-eslint/no-misused-promises */
3
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
4
+ import { MergeTranscriptsChange } from '@apollo-annotation/shared'
5
+ import { type AbstractSessionModel } from '@jbrowse/core/util'
6
+ import {
7
+ Box,
8
+ Button,
9
+ DialogActions,
10
+ DialogContent,
11
+ DialogContentText,
12
+ FormControl,
13
+ FormControlLabel,
14
+ Radio,
15
+ RadioGroup,
16
+ type SelectChangeEvent,
17
+ } from '@mui/material'
18
+ import { getSnapshot } from 'mobx-state-tree'
19
+ import React, { useState } from 'react'
20
+
21
+ import { type ChangeManager } from '../ChangeManager'
22
+ import { type ApolloSessionModel } from '../session'
23
+
24
+ import { Dialog } from './Dialog'
25
+
26
+ interface MergeTranscriptsProps {
27
+ session: ApolloSessionModel
28
+ handleClose(): void
29
+ sourceFeature: AnnotationFeature
30
+ sourceAssemblyId: string
31
+ changeManager: ChangeManager
32
+ selectedFeature?: AnnotationFeature
33
+ setSelectedFeature(feature?: AnnotationFeature): void
34
+ }
35
+
36
+ function getTranscripts(
37
+ referenceTranscript: AnnotationFeature,
38
+ session: ApolloSessionModel,
39
+ ): Record<string, AnnotationFeature> {
40
+ const gene = referenceTranscript.parent
41
+ if (!gene) {
42
+ throw new Error('Unable to find parent of reference transcript')
43
+ }
44
+
45
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
46
+ if (!featureTypeOntology) {
47
+ throw new Error('featureTypeOntology is undefined')
48
+ }
49
+
50
+ const transcripts: Record<string, AnnotationFeature> = {}
51
+ if (gene.children) {
52
+ for (const [, feature] of gene.children) {
53
+ if (
54
+ featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
55
+ feature._id !== referenceTranscript._id
56
+ ) {
57
+ transcripts[feature._id] = feature
58
+ }
59
+ }
60
+ }
61
+ return transcripts
62
+ }
63
+
64
+ function makeRadioButtonName(transcript: AnnotationFeature): string {
65
+ let id
66
+ if (transcript.attributes.get('gff_name')) {
67
+ id = transcript.attributes.get('gff_name')?.join(',')
68
+ } else if (transcript.attributes.get('gff_id')) {
69
+ id = transcript.attributes.get('gff_id')?.join(',')
70
+ } else {
71
+ id = transcript._id
72
+ }
73
+ return `${id} [${transcript.min + 1}-${transcript.max}]`
74
+ }
75
+
76
+ export function MergeTranscripts({
77
+ changeManager,
78
+ handleClose,
79
+ selectedFeature,
80
+ session,
81
+ setSelectedFeature,
82
+ sourceAssemblyId,
83
+ sourceFeature,
84
+ }: MergeTranscriptsProps) {
85
+ const { notify } = session as unknown as AbstractSessionModel
86
+ const [errorMessage, setErrorMessage] = useState('')
87
+ const [selectedTranscript, setSelectedTranscript] =
88
+ useState<AnnotationFeature>()
89
+
90
+ async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
91
+ event.preventDefault()
92
+ setErrorMessage('')
93
+ if (!selectedTranscript) {
94
+ return
95
+ }
96
+ if (selectedFeature?._id === sourceFeature._id) {
97
+ setSelectedFeature()
98
+ }
99
+
100
+ if (!sourceFeature.parent) {
101
+ throw new Error('Cannot find parent')
102
+ }
103
+
104
+ const change = new MergeTranscriptsChange({
105
+ changedIds: [sourceFeature._id],
106
+ typeName: 'MergeTranscriptsChange',
107
+ assembly: sourceAssemblyId,
108
+ firstTranscript: getSnapshot(sourceFeature),
109
+ secondTranscript: getSnapshot(selectedTranscript),
110
+ parentFeatureId: sourceFeature.parent._id,
111
+ })
112
+ await changeManager.submit(change)
113
+ notify('Transcripts successfully merged', 'success')
114
+ handleClose()
115
+ event.preventDefault()
116
+ }
117
+
118
+ const handleTypeChange = (e: SelectChangeEvent) => {
119
+ setErrorMessage('')
120
+ const { value } = e.target
121
+ setSelectedTranscript(transcripts[value])
122
+ }
123
+
124
+ const transcripts = getTranscripts(sourceFeature, session)
125
+
126
+ return (
127
+ <Dialog
128
+ open
129
+ title="Merge transcripts"
130
+ handleClose={handleClose}
131
+ maxWidth={false}
132
+ data-testid="merge-transcripts"
133
+ >
134
+ <form onSubmit={onSubmit}>
135
+ <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
136
+ {Object.keys(transcripts).length === 0
137
+ ? 'There are no transcripts to merge with'
138
+ : 'Merge with transcript:'}
139
+ <FormControl style={{ marginTop: 5 }}>
140
+ <RadioGroup
141
+ aria-labelledby="demo-radio-buttons-group-label"
142
+ name="radio-buttons-group"
143
+ value={selectedTranscript}
144
+ onChange={handleTypeChange}
145
+ >
146
+ {Object.keys(transcripts).map((key) => (
147
+ <FormControlLabel
148
+ value={key}
149
+ key={key}
150
+ control={<Radio />}
151
+ label={
152
+ <Box display="flex" alignItems="center">
153
+ {makeRadioButtonName(transcripts[key])}
154
+ </Box>
155
+ }
156
+ />
157
+ ))}
158
+ </RadioGroup>
159
+ </FormControl>
160
+ </DialogContent>
161
+
162
+ <DialogActions>
163
+ <Button
164
+ variant="contained"
165
+ type="submit"
166
+ disabled={
167
+ Object.keys(transcripts).length === 0 ||
168
+ selectedTranscript === undefined
169
+ }
170
+ >
171
+ Submit
172
+ </Button>
173
+ <Button variant="outlined" type="submit" onClick={handleClose}>
174
+ Cancel
175
+ </Button>
176
+ </DialogActions>
177
+ </form>
178
+ {errorMessage ? (
179
+ <DialogContent>
180
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
181
+ </DialogContent>
182
+ ) : null}
183
+ </Dialog>
184
+ )
185
+ }
@@ -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
+ }
@@ -9,7 +9,10 @@ export * from './ImportFeatures'
9
9
  export * from './LogOut'
10
10
  export * from './ManageChecks'
11
11
  export * from './ManageUsers'
12
+ export * from './MergeExons'
13
+ export * from './MergeTranscripts'
12
14
  export * from './OpenLocalFile'
13
15
  export * from './ViewChangeLog'
14
16
  export * from './AddRefSeqAliases'
15
17
  export * from './ViewCheckResults'
18
+ export * from './SplitExon'
package/src/config.ts CHANGED
@@ -10,6 +10,11 @@ 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
+ },
13
18
  })
14
19
 
15
20
  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
  )