@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,457 @@
1
+ import { AbstractSessionModel } from '@jbrowse/core/util'
2
+ import DeleteIcon from '@mui/icons-material/Delete'
3
+ import {
4
+ Button,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogContentText,
8
+ FormControl,
9
+ FormControlLabel,
10
+ FormLabel,
11
+ Grid,
12
+ IconButton,
13
+ Paper,
14
+ Radio,
15
+ RadioGroup,
16
+ TextField,
17
+ Typography,
18
+ } from '@mui/material'
19
+ import { AnnotationFeatureI } from 'apollo-mst'
20
+ import { FeatureAttributeChange } from 'apollo-shared'
21
+ import { getRoot, getSnapshot } from 'mobx-state-tree'
22
+ import React, { useMemo, useState } from 'react'
23
+ import { makeStyles } from 'tss-react/mui'
24
+
25
+ import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
26
+ import { ChangeManager } from '../ChangeManager'
27
+ import { ApolloSessionModel } from '../session'
28
+ import { ApolloRootModel } from '../types'
29
+ import { Dialog } from './Dialog'
30
+ import { OntologyTermMultiSelect } from './OntologyTermMultiSelect'
31
+
32
+ interface ModifyFeatureAttributeProps {
33
+ session: ApolloSessionModel
34
+ handleClose(): void
35
+ sourceFeature: AnnotationFeatureI
36
+ sourceAssemblyId: string
37
+ changeManager: ChangeManager
38
+ }
39
+
40
+ const reservedKeys = new Map([
41
+ [
42
+ 'Gene Ontology',
43
+ (props: AttributeValueEditorProps) => {
44
+ return <OntologyTermMultiSelect {...props} ontologyName="Gene Ontology" />
45
+ },
46
+ ],
47
+ [
48
+ 'Sequence Ontology',
49
+ (props: AttributeValueEditorProps) => {
50
+ return (
51
+ <OntologyTermMultiSelect {...props} ontologyName="Sequence Ontology" />
52
+ )
53
+ },
54
+ ],
55
+ ])
56
+
57
+ const useStyles = makeStyles()((theme) => ({
58
+ attributeInput: {
59
+ maxWidth: 600,
60
+ },
61
+ newAttributePaper: {
62
+ padding: theme.spacing(2),
63
+ },
64
+ attributeName: {
65
+ background: theme.palette.secondary.main,
66
+ color: theme.palette.secondary.contrastText,
67
+ padding: theme.spacing(1),
68
+ },
69
+ }))
70
+
71
+ export interface AttributeValueEditorProps {
72
+ session: ApolloSessionModel
73
+ value: string[]
74
+ onChange(newValue: string[]): void
75
+ }
76
+
77
+ const reservedTerms = [
78
+ 'ID',
79
+ 'Name',
80
+ 'Alias',
81
+ 'Target',
82
+ 'Gap',
83
+ 'Derives_from',
84
+ 'Note',
85
+ 'Dbxref',
86
+ 'Ontology',
87
+ 'Is_Circular',
88
+ ]
89
+
90
+ function CustomAttributeValueEditor(props: AttributeValueEditorProps) {
91
+ const { onChange, value } = props
92
+ return (
93
+ <TextField
94
+ type="text"
95
+ value={value}
96
+ onChange={(event) => {
97
+ onChange(event.target.value.split(','))
98
+ }}
99
+ variant="outlined"
100
+ fullWidth
101
+ helperText="Separate multiple values for the attribute with commas"
102
+ />
103
+ )
104
+ }
105
+
106
+ export interface GOTerm {
107
+ id: string
108
+ label: string
109
+ }
110
+
111
+ export function ModifyFeatureAttribute({
112
+ changeManager,
113
+ handleClose,
114
+ session,
115
+ sourceAssemblyId,
116
+ sourceFeature,
117
+ }: ModifyFeatureAttributeProps) {
118
+ const { notify } = session as unknown as AbstractSessionModel
119
+
120
+ const { internetAccounts } = getRoot<ApolloRootModel>(session)
121
+ const internetAccount = useMemo(() => {
122
+ return internetAccounts.find(
123
+ (ia) => ia.type === 'ApolloInternetAccount',
124
+ ) as ApolloInternetAccountModel | undefined
125
+ }, [internetAccounts])
126
+ const role = internetAccount ? internetAccount.role : 'admin'
127
+ const editable = ['admin', 'user'].includes(role ?? '')
128
+
129
+ const [errorMessage, setErrorMessage] = useState('')
130
+ const [attributes, setAttributes] = useState<Record<string, string[]>>(
131
+ Object.fromEntries(
132
+ [...sourceFeature.attributes.entries()].map(([key, value]) => {
133
+ if (key.startsWith('gff_')) {
134
+ const newKey = key.slice(4)
135
+ const capitalizedKey =
136
+ newKey.charAt(0).toUpperCase() + newKey.slice(1)
137
+ return [capitalizedKey, getSnapshot(value)]
138
+ }
139
+ if (key === '_id') {
140
+ return ['ID', getSnapshot(value)]
141
+ }
142
+ return [key, getSnapshot(value)]
143
+ }),
144
+ ),
145
+ )
146
+ const [showAddNewForm, setShowAddNewForm] = useState(false)
147
+ const [newAttributeKey, setNewAttributeKey] = useState('')
148
+ const { classes } = useStyles()
149
+ async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
150
+ event.preventDefault()
151
+ setErrorMessage('')
152
+
153
+ const attrs: Record<string, string[]> = {}
154
+ if (attributes) {
155
+ for (const [key, val] of Object.entries(attributes)) {
156
+ if (!val) {
157
+ continue
158
+ }
159
+ const newKey = key.toLowerCase()
160
+ if (newKey === 'parent') {
161
+ continue
162
+ }
163
+ if ([...reservedKeys.keys()].includes(key)) {
164
+ attrs[key] = val
165
+ continue
166
+ }
167
+ switch (key) {
168
+ case 'ID': {
169
+ attrs._id = val
170
+ break
171
+ }
172
+ case 'Name': {
173
+ attrs.gff_name = val
174
+ break
175
+ }
176
+ case 'Alias': {
177
+ attrs.gff_alias = val
178
+ break
179
+ }
180
+ case 'Target': {
181
+ attrs.gff_target = val
182
+ break
183
+ }
184
+ case 'Gap': {
185
+ attrs.gff_gap = val
186
+ break
187
+ }
188
+ case 'Derives_from': {
189
+ attrs.gff_derives_from = val
190
+ break
191
+ }
192
+ case 'Note': {
193
+ attrs.gff_note = val
194
+ break
195
+ }
196
+ case 'Dbxref': {
197
+ attrs.gff_dbxref = val
198
+ break
199
+ }
200
+ case 'Ontology_term': {
201
+ attrs.gff_ontology_term = val
202
+ break
203
+ }
204
+ case 'Is_circular': {
205
+ attrs.gff_is_circular = val
206
+ break
207
+ }
208
+ default: {
209
+ attrs[key.toLowerCase()] = val
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const change = new FeatureAttributeChange({
216
+ changedIds: [sourceFeature._id],
217
+ typeName: 'FeatureAttributeChange',
218
+ assembly: sourceAssemblyId,
219
+ featureId: sourceFeature._id,
220
+ attributes: attrs,
221
+ })
222
+ await changeManager.submit?.(change)
223
+ notify('Feature attributes modified successfully', 'success')
224
+ handleClose()
225
+ event.preventDefault()
226
+ }
227
+
228
+ function handleAddNewAttributeChange() {
229
+ setErrorMessage('')
230
+ if (newAttributeKey.trim().length === 0) {
231
+ setErrorMessage('Attribute key is mandatory')
232
+ return
233
+ }
234
+ if (newAttributeKey === 'Parent') {
235
+ setErrorMessage(
236
+ '"Parent" -key is handled internally and it cannot be modified manually',
237
+ )
238
+ return
239
+ }
240
+ if (newAttributeKey in attributes) {
241
+ setErrorMessage(`Attribute "${newAttributeKey}" already exists`)
242
+ return
243
+ }
244
+ if (
245
+ /^[A-Z]/.test(newAttributeKey) &&
246
+ !reservedTerms.includes(newAttributeKey) &&
247
+ ![...reservedKeys.keys()].includes(newAttributeKey)
248
+ ) {
249
+ setErrorMessage(
250
+ `Key cannot starts with uppercase letter unless key is one of these: ${reservedTerms.join(
251
+ ', ',
252
+ )}`,
253
+ )
254
+ return
255
+ }
256
+ setAttributes({ ...attributes, [newAttributeKey]: [] })
257
+ setShowAddNewForm(false)
258
+ setNewAttributeKey('')
259
+ }
260
+
261
+ function deleteAttribute(key: string) {
262
+ setErrorMessage('')
263
+ const { [key]: remove, ...rest } = attributes
264
+ setAttributes(rest)
265
+ }
266
+
267
+ function makeOnChange(id: string) {
268
+ return (newValue: string[]) => {
269
+ setAttributes({ ...attributes, [id]: newValue })
270
+ }
271
+ }
272
+
273
+ function handleRadioButtonChange(
274
+ event: React.ChangeEvent<HTMLInputElement>,
275
+ value: string,
276
+ ) {
277
+ if (value === 'custom') {
278
+ setNewAttributeKey('')
279
+ } else if (reservedKeys.has(value)) {
280
+ setNewAttributeKey(value)
281
+ } else {
282
+ setErrorMessage('Unknown attribute type')
283
+ }
284
+ }
285
+
286
+ const hasEmptyAttributes = Object.values(attributes).some(
287
+ (value) => value.length === 0 || value.includes(''),
288
+ )
289
+
290
+ return (
291
+ <Dialog
292
+ open
293
+ title="Feature attributes"
294
+ handleClose={handleClose}
295
+ maxWidth={false}
296
+ data-testid="modify-feature-attribute"
297
+ >
298
+ <form onSubmit={onSubmit}>
299
+ <DialogContent>
300
+ <Grid container direction="column" spacing={1}>
301
+ {Object.entries(attributes).map(([key, value]) => {
302
+ const EditorComponent =
303
+ reservedKeys.get(key) ?? CustomAttributeValueEditor
304
+ return (
305
+ <Grid container item spacing={3} alignItems="center" key={key}>
306
+ <Grid item xs="auto">
307
+ <Paper variant="outlined" className={classes.attributeName}>
308
+ <Typography>{key}</Typography>
309
+ </Paper>
310
+ </Grid>
311
+ <Grid item flexGrow={1}>
312
+ <EditorComponent
313
+ session={session}
314
+ value={value}
315
+ onChange={makeOnChange(key)}
316
+ />
317
+ </Grid>
318
+ <Grid item xs={1}>
319
+ <IconButton
320
+ aria-label="delete"
321
+ size="medium"
322
+ disabled={!editable}
323
+ onClick={() => {
324
+ deleteAttribute(key)
325
+ }}
326
+ >
327
+ <DeleteIcon fontSize="medium" key={key} />
328
+ </IconButton>
329
+ </Grid>
330
+ </Grid>
331
+ )
332
+ })}
333
+ <Grid item>
334
+ <Button
335
+ color="primary"
336
+ variant="contained"
337
+ disabled={showAddNewForm || !editable}
338
+ onClick={() => {
339
+ setShowAddNewForm(true)
340
+ }}
341
+ >
342
+ Add new
343
+ </Button>
344
+ </Grid>
345
+ {showAddNewForm ? (
346
+ <Grid item>
347
+ <Paper elevation={8} className={classes.newAttributePaper}>
348
+ <Grid container direction="column">
349
+ <Grid item>
350
+ <FormControl>
351
+ <FormLabel id="attribute-radio-button-group">
352
+ Select attribute type
353
+ </FormLabel>
354
+ <RadioGroup
355
+ aria-labelledby="demo-radio-buttons-group-label"
356
+ defaultValue="custom"
357
+ name="radio-buttons-group"
358
+ onChange={handleRadioButtonChange}
359
+ >
360
+ <FormControlLabel
361
+ value="custom"
362
+ control={<Radio />}
363
+ disableTypography
364
+ label={
365
+ <Grid container spacing={1} alignItems="center">
366
+ <Grid item>
367
+ <Typography>Custom</Typography>
368
+ </Grid>
369
+ <Grid item>
370
+ <TextField
371
+ label="Custom attribute key"
372
+ variant="outlined"
373
+ value={
374
+ reservedKeys.has(newAttributeKey)
375
+ ? ''
376
+ : newAttributeKey
377
+ }
378
+ disabled={reservedKeys.has(newAttributeKey)}
379
+ onChange={(event) => {
380
+ setNewAttributeKey(event.target.value)
381
+ }}
382
+ />
383
+ </Grid>
384
+ </Grid>
385
+ }
386
+ />
387
+ {[...reservedKeys.keys()].map((key) => (
388
+ <FormControlLabel
389
+ key={key}
390
+ value={key}
391
+ control={<Radio />}
392
+ label={key}
393
+ />
394
+ ))}
395
+ </RadioGroup>
396
+ </FormControl>
397
+ </Grid>
398
+ <Grid item>
399
+ <DialogActions>
400
+ <Button
401
+ key="addButton"
402
+ color="primary"
403
+ variant="contained"
404
+ style={{ margin: 2 }}
405
+ onClick={handleAddNewAttributeChange}
406
+ disabled={!newAttributeKey}
407
+ >
408
+ Add
409
+ </Button>
410
+ <Button
411
+ key="cancelAddButton"
412
+ variant="outlined"
413
+ type="submit"
414
+ onClick={() => {
415
+ setShowAddNewForm(false)
416
+ setNewAttributeKey('')
417
+ setErrorMessage('')
418
+ }}
419
+ >
420
+ Cancel
421
+ </Button>
422
+ </DialogActions>
423
+ </Grid>
424
+ </Grid>
425
+ </Paper>
426
+ </Grid>
427
+ ) : null}
428
+ </Grid>
429
+ {errorMessage ? (
430
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
431
+ ) : null}
432
+ </DialogContent>
433
+ <DialogActions>
434
+ <Button
435
+ variant="contained"
436
+ type="submit"
437
+ disabled={showAddNewForm || hasEmptyAttributes || !editable}
438
+ >
439
+ Submit changes
440
+ </Button>
441
+ <Button
442
+ variant="outlined"
443
+ type="submit"
444
+ disabled={showAddNewForm}
445
+ onClick={handleClose}
446
+ >
447
+ Cancel
448
+ </Button>
449
+ </DialogActions>
450
+ </form>
451
+
452
+ {/* <DialogContentText>
453
+ Separate multiple values for an attribute with a comma
454
+ </DialogContentText> */}
455
+ </Dialog>
456
+ )
457
+ }
@@ -0,0 +1,240 @@
1
+ import { AbstractSessionModel, isAbortException } from '@jbrowse/core/util'
2
+ import {
3
+ Autocomplete,
4
+ AutocompleteRenderInputParams,
5
+ TextField,
6
+ } from '@mui/material'
7
+ import React, { useCallback, useEffect, useState } from 'react'
8
+
9
+ import type { OntologyManager } from '../OntologyManager'
10
+ import { OntologyTerm, isDeprecated } from '../OntologyManager'
11
+ import OntologyStore from '../OntologyManager/OntologyStore'
12
+ import { ApolloSessionModel } from '../session'
13
+
14
+ interface OntologyTermAutocompleteProps {
15
+ session: ApolloSessionModel
16
+ ontologyName: string
17
+ ontologyVersion?: string
18
+ value: string
19
+ error?: boolean
20
+ filterTerms?: (term: OntologyTerm) => boolean
21
+ fetchValidTerms?: (
22
+ ontologyStore: OntologyStore,
23
+ signal: AbortSignal,
24
+ ) => Promise<OntologyTerm[] | undefined>
25
+ style?: React.CSSProperties
26
+ renderInput?: (
27
+ params: AutocompleteRenderInputParams & {
28
+ error?: boolean
29
+ errorMessage?: string
30
+ },
31
+ ) => React.ReactNode
32
+ onChange: (oldValue: string, newValue: string | null | undefined) => void
33
+ /** if true, include deprecated/obsolete terms */
34
+ includeDeprecated?: boolean
35
+ }
36
+
37
+ export function OntologyTermAutocomplete({
38
+ fetchValidTerms,
39
+ filterTerms: filterTermsProp,
40
+ includeDeprecated,
41
+ onChange,
42
+ ontologyName,
43
+ ontologyVersion,
44
+ renderInput,
45
+ session,
46
+ style,
47
+ value: valueString,
48
+ }: OntologyTermAutocompleteProps) {
49
+ const [open, setOpen] = useState(false)
50
+ const [termChoices, setTermChoices] = useState<OntologyTerm[] | undefined>()
51
+ const [currentOntologyTermInvalid, setCurrentOntologyTermInvalid] =
52
+ useState('')
53
+ const [currentOntologyTerm, setCurrentOntologyTerm] = useState<
54
+ OntologyTerm | undefined
55
+ >()
56
+
57
+ const ontologyManager = session.apolloDataStore
58
+ .ontologyManager as OntologyManager
59
+ const ontologyStore = ontologyManager.findOntology(
60
+ ontologyName,
61
+ ontologyVersion,
62
+ )?.dataStore
63
+
64
+ const needToLoadTermChoices = ontologyStore && open && !termChoices
65
+ const needToLoadCurrentTerm = ontologyStore && !currentOntologyTerm
66
+
67
+ const filterTerms = useCallback(
68
+ (term: OntologyTerm) =>
69
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
70
+ (includeDeprecated || !isDeprecated(term)) &&
71
+ (!filterTermsProp || filterTermsProp(term)),
72
+ [filterTermsProp, includeDeprecated],
73
+ )
74
+
75
+ // effect for clearing choices when not open
76
+ useEffect(() => {
77
+ if (!open) {
78
+ // eslint-disable-next-line unicorn/no-useless-undefined
79
+ setTermChoices(undefined)
80
+ }
81
+ }, [open])
82
+
83
+ // effect for matching the current value with an ontology term
84
+ useEffect(() => {
85
+ const controller = new AbortController()
86
+ const { signal } = controller
87
+ if (needToLoadCurrentTerm) {
88
+ setCurrentOntologyTermInvalid('')
89
+ getCurrentTerm(ontologyStore, valueString, filterTerms, signal).then(
90
+ (term) => {
91
+ if (!signal.aborted) {
92
+ setCurrentOntologyTerm(term)
93
+ }
94
+ },
95
+ (error) => {
96
+ if (!signal.aborted && !isAbortException(error)) {
97
+ setCurrentOntologyTermInvalid(String(error))
98
+ }
99
+ },
100
+ )
101
+ }
102
+ return () => {
103
+ controller.abort()
104
+ }
105
+ }, [session, valueString, filterTerms, ontologyStore, needToLoadCurrentTerm])
106
+
107
+ // effect for loading term autocompletions
108
+ useEffect(() => {
109
+ const controller = new AbortController()
110
+ const { signal } = controller
111
+ if (needToLoadTermChoices) {
112
+ getValidTerms(ontologyStore, fetchValidTerms, filterTerms, signal).then(
113
+ (soTerms) => {
114
+ if (soTerms && !signal.aborted) {
115
+ setTermChoices(soTerms)
116
+ }
117
+ },
118
+ (error) => {
119
+ if (!signal.aborted && !isAbortException(error)) {
120
+ ;(session as unknown as AbstractSessionModel).notify(
121
+ error.message,
122
+ 'error',
123
+ )
124
+ }
125
+ },
126
+ )
127
+ }
128
+ return () => {
129
+ controller.abort()
130
+ }
131
+ }, [
132
+ needToLoadTermChoices,
133
+ filterTerms,
134
+ ontologyStore,
135
+ session,
136
+ fetchValidTerms,
137
+ ])
138
+
139
+ const handleChange = async (
140
+ event: React.SyntheticEvent<Element, Event>,
141
+ newValue?: OntologyTerm | string | null,
142
+ ) => {
143
+ if (!newValue) {
144
+ return
145
+ }
146
+ if (typeof newValue === 'string') {
147
+ // eslint-disable-next-line unicorn/no-useless-undefined
148
+ setCurrentOntologyTerm(undefined)
149
+ onChange(valueString, newValue)
150
+ } else if (newValue.lbl !== valueString) {
151
+ setCurrentOntologyTermInvalid('')
152
+ setCurrentOntologyTerm(newValue)
153
+ onChange(valueString, newValue.lbl)
154
+ }
155
+ }
156
+
157
+ const extraTextFieldParams: { error?: boolean; helperText?: string } = {}
158
+ if (currentOntologyTermInvalid) {
159
+ extraTextFieldParams.error = true
160
+ extraTextFieldParams.helperText = currentOntologyTermInvalid
161
+ }
162
+
163
+ return (
164
+ <Autocomplete
165
+ style={style}
166
+ autoComplete
167
+ filterSelectedOptions
168
+ disableClearable
169
+ selectOnFocus
170
+ clearOnBlur
171
+ handleHomeEndKeys
172
+ freeSolo={true}
173
+ value={valueString}
174
+ options={termChoices ?? []}
175
+ onOpen={() => {
176
+ setOpen(true)
177
+ }}
178
+ onClose={() => {
179
+ setOpen(false)
180
+ }}
181
+ // noOptionsText={valueString ? 'No matches' : 'Start typing to search'}
182
+ loading={needToLoadTermChoices}
183
+ renderInput={
184
+ renderInput ??
185
+ ((params) => <TextField {...params} {...extraTextFieldParams} />)
186
+ }
187
+ getOptionLabel={(option) => {
188
+ if (typeof option === 'string') {
189
+ return option
190
+ }
191
+ return option.lbl ?? ''
192
+ }}
193
+ isOptionEqualToValue={(option, val) => option.lbl === val.lbl}
194
+ onChange={handleChange}
195
+ />
196
+ )
197
+ }
198
+
199
+ async function getCurrentTerm(
200
+ ontologyStore: OntologyStore,
201
+ currentTermLabel: string,
202
+ filterTerms: OntologyTermAutocompleteProps['filterTerms'],
203
+ _signal: AbortSignal,
204
+ ) {
205
+ if (!currentTermLabel) {
206
+ return
207
+ }
208
+
209
+ // TODO: support prefixed IDs as ontology terms here (e.g. SO:001234)
210
+ const terms = await ontologyStore.getTermsWithLabelOrSynonym(
211
+ currentTermLabel,
212
+ { includeSubclasses: false },
213
+ )
214
+ const term = terms.find(filterTerms ?? (() => true))
215
+ if (!term) {
216
+ throw new Error(`not a valid ${ontologyStore.ontologyName} term`)
217
+ }
218
+
219
+ return term
220
+ }
221
+
222
+ async function getValidTerms(
223
+ ontologyStore: OntologyStore,
224
+ fetchValidTerms: OntologyTermAutocompleteProps['fetchValidTerms'],
225
+ filterTerms: OntologyTermAutocompleteProps['filterTerms'],
226
+ signal: AbortSignal,
227
+ ) {
228
+ let result: OntologyTerm[] | undefined
229
+ if (fetchValidTerms) {
230
+ const customTermList = await fetchValidTerms(ontologyStore, signal)
231
+ if (customTermList) {
232
+ result = customTermList
233
+ }
234
+ }
235
+
236
+ if (!result) {
237
+ result = await ontologyStore.getAllTerms()
238
+ }
239
+ return filterTerms ? result.filter((element) => filterTerms(element)) : result
240
+ }