@apollo-annotation/jbrowse-plugin-apollo 0.1.0

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 (116) hide show
  1. package/README.md +76 -0
  2. package/dist/index.esm.js +10248 -0
  3. package/dist/index.esm.js.map +1 -0
  4. package/dist/index.js +7 -0
  5. package/dist/jbrowse-plugin-apollo.cjs.development.js +10298 -0
  6. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -0
  7. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +2 -0
  8. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -0
  9. package/dist/jbrowse-plugin-apollo.umd.development.js +46957 -0
  10. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -0
  11. package/dist/jbrowse-plugin-apollo.umd.production.min.js +2 -0
  12. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -0
  13. package/package.json +130 -0
  14. package/src/ApolloInternetAccount/addMenuItems.ts +94 -0
  15. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +121 -0
  16. package/src/ApolloInternetAccount/components/LoginButtons.tsx +62 -0
  17. package/src/ApolloInternetAccount/components/LoginIcons.tsx +74 -0
  18. package/src/ApolloInternetAccount/configSchema.ts +26 -0
  19. package/src/ApolloInternetAccount/index.ts +2 -0
  20. package/src/ApolloInternetAccount/model.ts +448 -0
  21. package/src/ApolloJobModel.ts +117 -0
  22. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +186 -0
  23. package/src/ApolloSequenceAdapter/configSchema.ts +12 -0
  24. package/src/ApolloSequenceAdapter/index.ts +21 -0
  25. package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +12 -0
  26. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +692 -0
  27. package/src/ApolloSixFrameRenderer/configSchema.ts +7 -0
  28. package/src/ApolloSixFrameRenderer/index.ts +3 -0
  29. package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +64 -0
  30. package/src/ApolloTextSearchAdapter/configSchema.ts +24 -0
  31. package/src/ApolloTextSearchAdapter/index.ts +18 -0
  32. package/src/BackendDrivers/BackendDriver.ts +31 -0
  33. package/src/BackendDrivers/CollaborationServerDriver.ts +318 -0
  34. package/src/BackendDrivers/DesktopFileDriver.ts +170 -0
  35. package/src/BackendDrivers/InMemoryFileDriver.ts +76 -0
  36. package/src/BackendDrivers/index.ts +4 -0
  37. package/src/ChangeManager.ts +148 -0
  38. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +248 -0
  39. package/src/LinearApolloDisplay/components/index.ts +1 -0
  40. package/src/LinearApolloDisplay/configSchema.ts +16 -0
  41. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +422 -0
  42. package/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts +1191 -0
  43. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +151 -0
  44. package/src/LinearApolloDisplay/glyphs/Glyph.ts +382 -0
  45. package/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts +697 -0
  46. package/src/LinearApolloDisplay/glyphs/index.ts +4 -0
  47. package/src/LinearApolloDisplay/index.ts +2 -0
  48. package/src/LinearApolloDisplay/stateModel/base.ts +146 -0
  49. package/src/LinearApolloDisplay/stateModel/getGlyph.ts +39 -0
  50. package/src/LinearApolloDisplay/stateModel/glyphs.ts +45 -0
  51. package/src/LinearApolloDisplay/stateModel/index.ts +20 -0
  52. package/src/LinearApolloDisplay/stateModel/layouts.ts +230 -0
  53. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +513 -0
  54. package/src/LinearApolloDisplay/stateModel/rendering.ts +441 -0
  55. package/src/LinearApolloDisplay/stateModel/trackHeightMixin.ts +43 -0
  56. package/src/LinearApolloDisplay/types.ts +1 -0
  57. package/src/OntologyManager/OntologyStore/__snapshots__/fulltext.test.ts.snap +208 -0
  58. package/src/OntologyManager/OntologyStore/__snapshots__/index.test.ts.snap +18846 -0
  59. package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +137 -0
  60. package/src/OntologyManager/OntologyStore/fulltext.test.ts +94 -0
  61. package/src/OntologyManager/OntologyStore/fulltext.ts +264 -0
  62. package/src/OntologyManager/OntologyStore/index.test.ts +130 -0
  63. package/src/OntologyManager/OntologyStore/index.ts +526 -0
  64. package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +89 -0
  65. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +180 -0
  66. package/src/OntologyManager/OntologyStore/obo-graph-json-schema.ts +110 -0
  67. package/src/OntologyManager/OntologyStore/prefixes.ts +35 -0
  68. package/src/OntologyManager/index.ts +173 -0
  69. package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +19 -0
  70. package/src/SixFrameFeatureDisplay/components/index.ts +1 -0
  71. package/src/SixFrameFeatureDisplay/configSchema.ts +21 -0
  72. package/src/SixFrameFeatureDisplay/index.ts +2 -0
  73. package/src/SixFrameFeatureDisplay/stateModel.ts +413 -0
  74. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +88 -0
  75. package/src/TabularEditor/HybridGrid/Feature.tsx +346 -0
  76. package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +34 -0
  77. package/src/TabularEditor/HybridGrid/Highlight.tsx +40 -0
  78. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +138 -0
  79. package/src/TabularEditor/HybridGrid/NumberCell.tsx +77 -0
  80. package/src/TabularEditor/HybridGrid/ToolBar.tsx +59 -0
  81. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +119 -0
  82. package/src/TabularEditor/HybridGrid/index.ts +1 -0
  83. package/src/TabularEditor/TabularEditorPane.tsx +34 -0
  84. package/src/TabularEditor/index.ts +3 -0
  85. package/src/TabularEditor/model.ts +44 -0
  86. package/src/TabularEditor/types.ts +3 -0
  87. package/src/components/AddAssembly.tsx +464 -0
  88. package/src/components/AddChildFeature.tsx +247 -0
  89. package/src/components/AddFeature.tsx +252 -0
  90. package/src/components/CopyFeature.tsx +328 -0
  91. package/src/components/DeleteAssembly.tsx +185 -0
  92. package/src/components/DeleteFeature.tsx +90 -0
  93. package/src/components/Dialog.tsx +47 -0
  94. package/src/components/DownloadGFF3.tsx +213 -0
  95. package/src/components/ImportFeatures.tsx +295 -0
  96. package/src/components/ManageChecks.tsx +280 -0
  97. package/src/components/ManageUsers.tsx +218 -0
  98. package/src/components/ModifyFeatureAttribute.tsx +457 -0
  99. package/src/components/OntologyTermAutocomplete.tsx +240 -0
  100. package/src/components/OntologyTermMultiSelect.tsx +349 -0
  101. package/src/components/OpenLocalFile.tsx +178 -0
  102. package/src/components/ViewChangeLog.tsx +208 -0
  103. package/src/components/ViewCheckResults.tsx +151 -0
  104. package/src/components/index.ts +12 -0
  105. package/src/config.ts +10 -0
  106. package/src/declare.d.ts +3 -0
  107. package/src/extensions/annotationFromPileup.ts +208 -0
  108. package/src/extensions/index.ts +1 -0
  109. package/src/index.ts +394 -0
  110. package/src/makeDisplayComponent.tsx +244 -0
  111. package/src/session/ClientDataStore.ts +282 -0
  112. package/src/session/index.ts +1 -0
  113. package/src/session/session.ts +373 -0
  114. package/src/types.ts +10 -0
  115. package/src/util/index.ts +31 -0
  116. package/src/util/loadAssemblyIntoClient.ts +291 -0
@@ -0,0 +1,346 @@
1
+ import { AbstractSessionModel } from '@jbrowse/core/util'
2
+ import { AnnotationFeatureI } from 'apollo-mst'
3
+ import { observer } from 'mobx-react'
4
+ import React from 'react'
5
+ import { makeStyles } from 'tss-react/mui'
6
+
7
+ import { OntologyTermAutocomplete } from '../../components/OntologyTermAutocomplete'
8
+ import { isOntologyClass } from '../../OntologyManager'
9
+ import OntologyStore from '../../OntologyManager/OntologyStore'
10
+ import { DisplayStateModel } from '../types'
11
+ import {
12
+ handleFeatureEndChange,
13
+ handleFeatureStartChange,
14
+ handleFeatureTypeChange,
15
+ } from './ChangeHandling'
16
+ import { FeatureAttributes } from './FeatureAttributes'
17
+ import { featureContextMenuItems } from './featureContextMenuItems'
18
+ import type { ContextMenuState } from './HybridGrid'
19
+ import { NumberCell } from './NumberCell'
20
+
21
+ const useStyles = makeStyles()((theme) => ({
22
+ typeContent: {
23
+ display: 'inline-block',
24
+ width: '174px',
25
+ height: '100%',
26
+ cursor: 'text',
27
+ },
28
+ feature: {
29
+ td: {
30
+ position: 'relative',
31
+ verticalAlign: 'top',
32
+ paddingLeft: '0.5em',
33
+ },
34
+ },
35
+ arrow: {
36
+ display: 'inline-block',
37
+ width: '1.6em',
38
+ textAlign: 'center',
39
+ cursor: 'pointer',
40
+ },
41
+ arrowExpanded: {
42
+ transform: 'rotate(90deg)',
43
+ },
44
+ hoveredFeature: {
45
+ backgroundColor: theme.palette.action.hover,
46
+ },
47
+ typeInputElement: {
48
+ border: 'none',
49
+ background: 'none',
50
+ },
51
+ typeErrorMessage: {
52
+ color: 'red',
53
+ },
54
+ }))
55
+
56
+ function makeContextMenuItems(
57
+ display: DisplayStateModel,
58
+ feature: AnnotationFeatureI,
59
+ ) {
60
+ const {
61
+ changeManager,
62
+ getAssemblyId,
63
+ regions,
64
+ selectedFeature,
65
+ session,
66
+ setSelectedFeature,
67
+ } = display
68
+ return featureContextMenuItems(
69
+ feature,
70
+ regions[0],
71
+ getAssemblyId,
72
+ selectedFeature,
73
+ setSelectedFeature,
74
+ session,
75
+ changeManager,
76
+ )
77
+ }
78
+
79
+ function getTopLevelFeature(feature: AnnotationFeatureI): AnnotationFeatureI {
80
+ let cur = feature
81
+ while (cur.parent) {
82
+ cur = cur.parent
83
+ }
84
+ return cur
85
+ }
86
+
87
+ export const Feature = observer(function Feature({
88
+ depth,
89
+ feature,
90
+ isHovered,
91
+ isSelected,
92
+ model: displayState,
93
+ selectedFeatureClass,
94
+ setContextMenu,
95
+ }: {
96
+ model: DisplayStateModel
97
+ feature: AnnotationFeatureI
98
+ depth: number
99
+ isHovered: boolean
100
+ isSelected: boolean
101
+ selectedFeatureClass: string
102
+ setContextMenu: (menu: ContextMenuState) => void
103
+ }) {
104
+ const { classes } = useStyles()
105
+ const {
106
+ apolloHover,
107
+ changeManager,
108
+ selectedFeature,
109
+ session,
110
+ tabularEditor: tabularEditorState,
111
+ } = displayState
112
+ const { featureCollapsed, filterText } = tabularEditorState
113
+ const {
114
+ _id,
115
+ children,
116
+ discontinuousLocations,
117
+ end,
118
+ phase,
119
+ start,
120
+ strand,
121
+ type,
122
+ } = feature
123
+ const expanded = !featureCollapsed.get(_id)
124
+ const toggleExpanded = (e: React.MouseEvent) => {
125
+ e.stopPropagation()
126
+ tabularEditorState.setFeatureCollapsed(_id, expanded)
127
+ }
128
+
129
+ // pop up a snackbar in the session notifying user of an error
130
+ const notifyError = (e: Error) =>
131
+ (session as unknown as AbstractSessionModel).notify(e.message, 'error')
132
+
133
+ return (
134
+ <>
135
+ <tr
136
+ onMouseEnter={(_e) => {
137
+ displayState.setApolloHover({
138
+ feature,
139
+ topLevelFeature: getTopLevelFeature(feature),
140
+ })
141
+ }}
142
+ className={
143
+ classes.feature +
144
+ (isSelected
145
+ ? ` ${selectedFeatureClass}`
146
+ : isHovered
147
+ ? ` ${classes.hoveredFeature}`
148
+ : '')
149
+ }
150
+ onClick={(e) => {
151
+ e.stopPropagation()
152
+ displayState.setSelectedFeature(feature)
153
+ }}
154
+ onContextMenu={(e) => {
155
+ e.preventDefault()
156
+ setContextMenu({
157
+ position: { left: e.clientX + 2, top: e.clientY - 6 },
158
+ items: makeContextMenuItems(displayState, feature),
159
+ })
160
+ return false
161
+ }}
162
+ >
163
+ <td
164
+ style={{
165
+ whiteSpace: 'nowrap',
166
+ borderLeft: `${depth * 2}em solid transparent`,
167
+ }}
168
+ >
169
+ {children?.size ? (
170
+ // TODO: a11y
171
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
172
+ <div
173
+ onClick={toggleExpanded}
174
+ className={
175
+ classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '')
176
+ }
177
+ >
178
+
179
+ </div>
180
+ ) : null}
181
+ <div className={classes.typeContent}>
182
+ <OntologyTermAutocomplete
183
+ session={session}
184
+ ontologyName="Sequence Ontology"
185
+ style={{ width: 170 }}
186
+ value={type}
187
+ filterTerms={isOntologyClass}
188
+ fetchValidTerms={fetchValidTypeTerms.bind(null, feature)}
189
+ renderInput={(params) => {
190
+ return (
191
+ <div ref={params.InputProps.ref}>
192
+ <input
193
+ type="text"
194
+ {...params.inputProps}
195
+ className={classes.typeInputElement}
196
+ style={{ width: 170 }}
197
+ />
198
+ {params.error ? (
199
+ <div className={classes.typeErrorMessage}>
200
+ {params.errorMessage ?? 'unknown error'}
201
+ </div>
202
+ ) : null}
203
+ </div>
204
+ )
205
+ }}
206
+ onChange={(oldValue, newValue) => {
207
+ if (newValue) {
208
+ handleFeatureTypeChange(
209
+ changeManager,
210
+ feature,
211
+ oldValue,
212
+ newValue,
213
+ ).catch(notifyError)
214
+ }
215
+ }}
216
+ />
217
+ </div>
218
+ </td>
219
+ <td>
220
+ {discontinuousLocations && discontinuousLocations.length > 0 ? (
221
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
222
+ {discontinuousLocations.map((loc, index) => (
223
+ <NumberCell
224
+ key={`${_id}:${loc.start},${loc.phase}`}
225
+ initialValue={loc.start + 1}
226
+ notifyError={notifyError}
227
+ onChangeCommitted={(newStart) =>
228
+ handleFeatureStartChange(
229
+ changeManager,
230
+ feature,
231
+ discontinuousLocations[index].start,
232
+ newStart - 1,
233
+ index,
234
+ )
235
+ }
236
+ />
237
+ ))}
238
+ </div>
239
+ ) : (
240
+ <NumberCell
241
+ initialValue={start + 1}
242
+ notifyError={notifyError}
243
+ onChangeCommitted={(newStart) =>
244
+ handleFeatureStartChange(
245
+ changeManager,
246
+ feature,
247
+ start,
248
+ newStart - 1,
249
+ )
250
+ }
251
+ />
252
+ )}
253
+ </td>
254
+ <td>
255
+ {discontinuousLocations && discontinuousLocations.length > 0 ? (
256
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
257
+ {discontinuousLocations.map((loc, index) => (
258
+ <NumberCell
259
+ key={`${_id}:${loc.end},${loc.phase}`}
260
+ initialValue={loc.end}
261
+ notifyError={notifyError}
262
+ onChangeCommitted={(newEnd) =>
263
+ handleFeatureEndChange(
264
+ changeManager,
265
+ feature,
266
+ discontinuousLocations[index].end,
267
+ newEnd,
268
+ index,
269
+ )
270
+ }
271
+ />
272
+ ))}
273
+ </div>
274
+ ) : (
275
+ <NumberCell
276
+ initialValue={end}
277
+ notifyError={notifyError}
278
+ onChangeCommitted={(newEnd) =>
279
+ handleFeatureEndChange(changeManager, feature, end, newEnd)
280
+ }
281
+ />
282
+ )}
283
+ </td>
284
+ <td>{strand === 1 ? '+' : strand === -1 ? '-' : undefined}</td>
285
+ <td>{phase}</td>
286
+ <td>
287
+ <FeatureAttributes filterText={filterText} feature={feature} />
288
+ </td>
289
+ </tr>
290
+ {expanded && children
291
+ ? [...children.entries()]
292
+ .filter((entry) => {
293
+ if (!filterText) {
294
+ return true
295
+ }
296
+ const [, childFeature] = entry
297
+ // search feature and its subfeatures for the text
298
+ const text = JSON.stringify(childFeature)
299
+ return text.includes(filterText)
300
+ })
301
+ .map(([featureId, childFeature]) => {
302
+ const childHovered =
303
+ apolloHover?.feature?._id === childFeature._id
304
+ const childSelected = selectedFeature?._id === childFeature._id
305
+ return (
306
+ <Feature
307
+ isHovered={childHovered}
308
+ isSelected={childSelected}
309
+ selectedFeatureClass={selectedFeatureClass}
310
+ key={featureId}
311
+ depth={(depth || 0) + 1}
312
+ feature={childFeature}
313
+ model={displayState}
314
+ setContextMenu={setContextMenu}
315
+ />
316
+ )
317
+ })
318
+ : null}
319
+ </>
320
+ )
321
+ })
322
+ async function fetchValidTypeTerms(
323
+ feature: AnnotationFeatureI,
324
+ ontologyStore: OntologyStore,
325
+ _signal: AbortSignal,
326
+ ) {
327
+ const { parent: parentFeature } = feature
328
+ if (parentFeature) {
329
+ // if this is a child of an existing feature, restrict the autocomplete choices to valid
330
+ // parts of that feature
331
+ const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(
332
+ parentFeature.type,
333
+ { includeSubclasses: false },
334
+ )
335
+ // eslint-disable-next-line unicorn/no-array-callback-reference
336
+ const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass)
337
+ if (parentTypeClassTerms.length > 0) {
338
+ const subpartTerms = await ontologyStore.getClassesThat(
339
+ 'part_of',
340
+ parentTypeClassTerms,
341
+ )
342
+ return subpartTerms
343
+ }
344
+ }
345
+ return
346
+ }
@@ -0,0 +1,34 @@
1
+ import { AnnotationFeatureI } from 'apollo-mst'
2
+ import { observer } from 'mobx-react'
3
+ import { getSnapshot } from 'mobx-state-tree'
4
+ import React from 'react'
5
+
6
+ import Highlight from './Highlight'
7
+
8
+ export const FeatureAttributes = observer(function FeatureAttributes({
9
+ feature,
10
+ filterText,
11
+ }: {
12
+ feature: AnnotationFeatureI
13
+ filterText: string
14
+ }) {
15
+ const attrString = [...feature.attributes.entries()]
16
+ .map(([key, value]) => {
17
+ if (key.startsWith('gff_')) {
18
+ const newKey = key.slice(4)
19
+ const capitalizedKey = newKey.charAt(0).toUpperCase() + newKey.slice(1)
20
+ return [capitalizedKey, getSnapshot(value)]
21
+ }
22
+ if (key === '_id') {
23
+ return ['ID', getSnapshot(value)]
24
+ }
25
+ return [key, getSnapshot(value)]
26
+ })
27
+ .map(
28
+ ([key, values]) =>
29
+ `${key}=${Array.isArray(values) ? values.join(', ') : values}`,
30
+ )
31
+ .join(', ')
32
+
33
+ return <Highlight text={attrString} highlight={filterText} />
34
+ })
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+ import { makeStyles } from 'tss-react/mui'
3
+
4
+ const useStyles = makeStyles()({
5
+ highlighted: {
6
+ background: 'orange',
7
+ },
8
+ })
9
+
10
+ const Highlight = ({
11
+ highlight,
12
+ text,
13
+ }: {
14
+ text: string
15
+ highlight: string
16
+ }) => {
17
+ const { classes } = useStyles()
18
+ if (!highlight) {
19
+ return <>{text}</>
20
+ }
21
+ const split = text.split(highlight)
22
+ if (split.length === 1) {
23
+ return <>{text}</>
24
+ }
25
+ const highlighted: React.ReactNode[] = []
26
+ for (let i = 0; i < split.length - 1; i++) {
27
+ highlighted.push(
28
+ split[i],
29
+ <span className={classes.highlighted}>{highlight}</span>,
30
+ )
31
+ }
32
+ return (
33
+ <>
34
+ {highlighted}
35
+ {split.at(-1)}
36
+ </>
37
+ )
38
+ }
39
+
40
+ export default Highlight
@@ -0,0 +1,138 @@
1
+ import { Menu, MenuItem } from '@jbrowse/core/ui'
2
+ import { useTheme } from '@mui/material'
3
+ import { observer } from 'mobx-react'
4
+ import React, { useEffect, useRef, useState } from 'react'
5
+ import { makeStyles } from 'tss-react/mui'
6
+
7
+ import { DisplayStateModel } from '../types'
8
+ import { Feature } from './Feature'
9
+
10
+ const useStyles = makeStyles()((theme) => ({
11
+ scrollableTable: {
12
+ width: '100%',
13
+ height: '100%',
14
+ th: {
15
+ position: 'sticky',
16
+ top: 0,
17
+ zIndex: 2,
18
+ textAlign: 'left',
19
+ background: theme.palette.background.paper,
20
+ paddingTop: '3.2em',
21
+ },
22
+ td: { whiteSpace: 'normal' },
23
+ },
24
+ selectedFeature: {
25
+ backgroundColor: theme.palette.action.selected,
26
+ },
27
+ }))
28
+
29
+ export type ContextMenuState = null | {
30
+ position: { top: number; left: number }
31
+ items: MenuItem[]
32
+ }
33
+
34
+ const HybridGrid = observer(function HybridGrid({
35
+ model,
36
+ }: {
37
+ model: DisplayStateModel
38
+ }) {
39
+ const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model
40
+ const theme = useTheme()
41
+ const { classes } = useStyles()
42
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
43
+ const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
44
+
45
+ const { filterText } = tabularEditor
46
+
47
+ // scrolls to selected feature if one is selected and it's not already visible
48
+ useEffect(() => {
49
+ const scrollContainer = scrollContainerRef.current
50
+ if (scrollContainer && selectedFeature) {
51
+ const selectedRow = scrollContainer.querySelector(
52
+ `.${classes.selectedFeature}`,
53
+ ) as HTMLElement | null
54
+ if (selectedRow) {
55
+ const currScroll = scrollContainer.scrollTop
56
+ const newScrollTop = selectedRow.offsetTop - 25
57
+ const isVisible =
58
+ newScrollTop > currScroll &&
59
+ newScrollTop < currScroll + scrollContainer.offsetHeight
60
+ if (!isVisible) {
61
+ scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' })
62
+ }
63
+ }
64
+ }
65
+ }, [selectedFeature, seenFeatures, classes.selectedFeature])
66
+
67
+ return (
68
+ <div
69
+ ref={scrollContainerRef}
70
+ style={{ width: '100%', overflowY: 'auto', height: '100%' }}
71
+ >
72
+ <table className={classes.scrollableTable}>
73
+ <thead>
74
+ <tr>
75
+ <th>Type</th>
76
+ <th>Start</th>
77
+ <th>End</th>
78
+ <th>Strand</th>
79
+ <th>Phase</th>
80
+ <th>Attributes</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {[...seenFeatures.entries()]
85
+ .filter((entry) => {
86
+ if (!filterText) {
87
+ return true
88
+ }
89
+ const [, feature] = entry
90
+ // search feature and its subfeatures for the text
91
+ const text = JSON.stringify(feature)
92
+ return text.includes(filterText)
93
+ })
94
+ .sort((a, b) => {
95
+ return a[1].start - b[1].start
96
+ })
97
+ .map(([featureId, feature]) => {
98
+ const isSelected = selectedFeature?._id === featureId
99
+ const isHovered = apolloHover?.feature?._id === featureId
100
+ return (
101
+ <Feature
102
+ key={featureId}
103
+ isSelected={isSelected}
104
+ isHovered={isHovered}
105
+ selectedFeatureClass={classes.selectedFeature}
106
+ feature={feature}
107
+ model={model}
108
+ depth={0}
109
+ setContextMenu={setContextMenu}
110
+ />
111
+ )
112
+ })}
113
+ </tbody>
114
+ </table>
115
+ <Menu
116
+ open={Boolean(contextMenu)}
117
+ onMenuItemClick={(_, callback) => {
118
+ callback()
119
+ setContextMenu(null)
120
+ }}
121
+ onClose={() => {
122
+ setContextMenu(null)
123
+ }}
124
+ TransitionProps={{
125
+ onExit: () => {
126
+ setContextMenu(null)
127
+ },
128
+ }}
129
+ style={{ zIndex: theme.zIndex.tooltip }}
130
+ menuItems={contextMenu?.items ?? []}
131
+ anchorReference="anchorPosition"
132
+ anchorPosition={contextMenu?.position}
133
+ />
134
+ </div>
135
+ )
136
+ })
137
+
138
+ export default HybridGrid
@@ -0,0 +1,77 @@
1
+ import { observer } from 'mobx-react'
2
+ import React, { useEffect, useState } from 'react'
3
+ import { makeStyles } from 'tss-react/mui'
4
+
5
+ const useStyles = makeStyles()((theme) => ({
6
+ inputWrapper: {
7
+ position: 'relative',
8
+ },
9
+ hiddenWidthSpan: {
10
+ padding: theme.spacing(0.5),
11
+ color: 'transparent',
12
+ },
13
+ numberTextInput: {
14
+ border: 'none',
15
+ background: 'inherit',
16
+ font: 'inherit',
17
+ position: 'absolute',
18
+ width: '100%',
19
+ left: 0,
20
+ },
21
+ }))
22
+
23
+ interface NumberCellProps {
24
+ initialValue: number
25
+ notifyError(error: Error): void
26
+ onChangeCommitted(newValue: number): Promise<void>
27
+ }
28
+
29
+ export const NumberCell = observer(function NumberCell({
30
+ initialValue,
31
+ notifyError,
32
+ onChangeCommitted,
33
+ }: NumberCellProps) {
34
+ const [value, setValue] = useState(initialValue)
35
+ const [blur, setBlur] = useState(false)
36
+ const [inputNode, setInputNode] = useState<HTMLInputElement | null>(null)
37
+ const { classes } = useStyles()
38
+ useEffect(() => {
39
+ if (blur) {
40
+ inputNode?.blur()
41
+ setBlur(false)
42
+ }
43
+ }, [blur, inputNode])
44
+ function onChange(event: React.ChangeEvent<HTMLInputElement>) {
45
+ const newValue = Number(event.target.value)
46
+ if (!Number.isNaN(newValue)) {
47
+ setValue(newValue)
48
+ }
49
+ }
50
+ return (
51
+ <span className={classes.inputWrapper}>
52
+ <span className={classes.hiddenWidthSpan} aria-hidden>
53
+ {value}
54
+ </span>
55
+ <input
56
+ type="text"
57
+ value={value}
58
+ className={classes.numberTextInput}
59
+ onChange={onChange}
60
+ onKeyDown={(event) => {
61
+ if (event.key === 'Enter') {
62
+ inputNode?.blur()
63
+ } else if (event.key === 'Escape') {
64
+ setValue(initialValue)
65
+ setBlur(true)
66
+ }
67
+ }}
68
+ onBlur={() => {
69
+ if (value !== initialValue) {
70
+ onChangeCommitted(value).catch(notifyError)
71
+ }
72
+ }}
73
+ ref={(node) => setInputNode(node)}
74
+ />
75
+ </span>
76
+ )
77
+ })
@@ -0,0 +1,59 @@
1
+ import ClearIcon from '@mui/icons-material/Clear'
2
+ import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'
3
+ import { IconButton, InputAdornment, TextField, Tooltip } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+ import React from 'react'
6
+ import { makeStyles } from 'tss-react/mui'
7
+
8
+ import { DisplayStateModel } from '../types'
9
+
10
+ const useStyles = makeStyles()({
11
+ toolbar: {
12
+ width: '100%',
13
+ display: 'flex',
14
+ paddingRight: '2em',
15
+ flexDirection: 'row',
16
+ justifyContent: 'space-between',
17
+ position: 'absolute',
18
+ zIndex: 4,
19
+ },
20
+ filterText: {},
21
+ })
22
+ export const ToolBar = observer(function ToolBar({
23
+ model: displayState,
24
+ }: {
25
+ model: DisplayStateModel
26
+ }) {
27
+ const model = displayState.tabularEditor
28
+ const { classes } = useStyles()
29
+ return (
30
+ <div className={classes.toolbar}>
31
+ <Tooltip title="Collapse all">
32
+ <IconButton
33
+ aria-label="collapse"
34
+ sx={{ marginTop: 0 }}
35
+ onClick={model.collapseAllFeatures}
36
+ >
37
+ <UnfoldLessIcon />
38
+ </IconButton>
39
+ </Tooltip>
40
+ <TextField
41
+ className={classes.filterText}
42
+ label="Filter features"
43
+ value={model.filterText}
44
+ sx={{ marginTop: 0 }}
45
+ variant="outlined"
46
+ onChange={(event) => model.setFilterText(event.target.value)}
47
+ InputProps={{
48
+ endAdornment: (
49
+ <InputAdornment position="end">
50
+ <IconButton onClick={() => model.clearFilterText()}>
51
+ <ClearIcon />
52
+ </IconButton>
53
+ </InputAdornment>
54
+ ),
55
+ }}
56
+ />
57
+ </div>
58
+ )
59
+ })