@apollo-annotation/jbrowse-plugin-apollo 0.3.8 → 0.3.9

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 (48) hide show
  1. package/dist/index.esm.js +10932 -10932
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +10845 -10846
  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 +18619 -21342
  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 +7 -7
  12. package/src/ApolloInternetAccount/model.ts +81 -63
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +49 -18
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +3 -1
  18. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  19. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  20. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +39 -203
  21. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  22. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -102
  23. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +31 -230
  24. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  25. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  26. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  27. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +62 -386
  28. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +6 -0
  29. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +122 -70
  30. package/src/components/AddAssembly.tsx +33 -37
  31. package/src/components/AddFeature.tsx +21 -18
  32. package/src/components/AddRefSeqAliases.tsx +56 -42
  33. package/src/components/CopyFeature.tsx +1 -1
  34. package/src/components/CreateApolloAnnotation.tsx +22 -10
  35. package/src/components/DeleteAssembly.tsx +2 -9
  36. package/src/components/DownloadGFF3.tsx +2 -2
  37. package/src/components/ManageChecks.tsx +2 -9
  38. package/src/components/ManageUsers.tsx +23 -22
  39. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  40. package/src/components/ViewChangeLog.tsx +25 -50
  41. package/src/components/ViewCheckResults.tsx +1 -7
  42. package/src/config.ts +3 -3
  43. package/src/index.ts +17 -16
  44. package/src/makeDisplayComponent.tsx +9 -13
  45. package/src/session/ClientDataStore.ts +32 -14
  46. package/src/session/session.ts +19 -27
  47. package/src/util/glyphUtils.ts +178 -1
  48. package/src/util/loadAssemblyIntoClient.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/jbrowse-plugin-apollo",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Apollo plugin for JBrowse 2",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -27,7 +27,7 @@
27
27
  "start:watch": "JB_NPM=false NODE_ENV=development rollup --config --watch",
28
28
  "start:server": "serve --no-request-logging --cors --listen 9000 --no-port-switching .",
29
29
  "build": "yarn build:shared && yarn clean && rollup --config",
30
- "browse": "serve --no-request-logging --listen 8999 --no-port-switching .jbrowse",
30
+ "browse": "serve --no-request-logging --listen 8999 --no-port-switching --symlinks .jbrowse",
31
31
  "test": "jest",
32
32
  "test:ci": "jest --coverage",
33
33
  "start:collab-cypress": "yarn workspace @apollo-annotation/collaboration-server run cypress:start",
@@ -48,12 +48,12 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@apollo-annotation/common": "^0.3.8",
52
- "@apollo-annotation/mst": "^0.3.8",
53
- "@apollo-annotation/shared": "^0.3.8",
51
+ "@apollo-annotation/common": "^0.3.9",
52
+ "@apollo-annotation/mst": "^0.3.9",
53
+ "@apollo-annotation/shared": "^0.3.9",
54
54
  "@emotion/react": "^11.10.6",
55
55
  "@emotion/styled": "^11.10.6",
56
- "@gmod/gff": "1.2.0",
56
+ "@gmod/gff": "^2.0.0",
57
57
  "@jbrowse/plugin-authentication": "^3.6.5",
58
58
  "@jbrowse/plugin-linear-genome-view": "^3.6.5",
59
59
  "@mui/icons-material": "^6.5.0",
@@ -79,7 +79,7 @@
79
79
  "@mui/x-data-grid": "^8.0.0",
80
80
  "@types/autosuggest-highlight": "^3",
81
81
  "@types/file-saver": "^2",
82
- "@types/node": "^18.14.2",
82
+ "@types/node": "^20.19.15",
83
83
  "@types/prop-types": "^15",
84
84
  "@types/react": "^18.3.4",
85
85
  "@types/react-dom": "^18",
@@ -64,32 +64,20 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
64
64
  controller: new AbortController(),
65
65
  }))
66
66
 
67
- .actions((self) => {
68
- let roleNotificationSent = false
69
- return {
70
- setRole() {
71
- const token = self.retrieveToken()
72
- if (!token) {
73
- self.role = undefined
74
- return
75
- }
76
- const dec = getDecodedToken(token)
77
- const { role } = dec
78
- if (!role && !roleNotificationSent) {
79
- const { session } = getRoot<ApolloRootModel>(self)
80
- ;(session as unknown as AbstractSessionModel).notify(
81
- 'You have registered as a user but have not been given access. Ask your administrator to enable access for your account.',
82
- 'warning',
83
- )
84
- // notify
85
- roleNotificationSent = true
86
- }
87
- if (self.role !== role) {
88
- self.role = role
89
- }
90
- },
91
- }
92
- })
67
+ .actions((self) => ({
68
+ setRole() {
69
+ const token = self.retrieveToken()
70
+ if (!token) {
71
+ self.role = undefined
72
+ return
73
+ }
74
+ const dec = getDecodedToken(token)
75
+ const { role } = dec
76
+ if (self.role !== role) {
77
+ self.role = role
78
+ }
79
+ },
80
+ }))
93
81
  .actions((self) => {
94
82
  let listener: (event: MessageEvent) => void
95
83
  return {
@@ -380,7 +368,7 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
380
368
  }))
381
369
  .actions((self) => {
382
370
  async function postUserLocation(userLoc: UserLocation[]) {
383
- if (!isAlive(self)) {
371
+ if (!isAlive(self) || self.role === 'none') {
384
372
  return
385
373
  }
386
374
  const { baseURL, controller } = self
@@ -418,45 +406,73 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
418
406
  }
419
407
  return { postUserLocation: debouncePostUserLocation(postUserLocation) }
420
408
  })
421
- .actions((self) => ({
422
- initialize: flow(function* initialize(role: Role) {
423
- if (role === 'admin') {
424
- const rootModel = getRoot(self)
425
- if (isAbstractMenuManager(rootModel)) {
426
- addTopLevelAdminMenus(rootModel)
427
- }
428
- }
429
- // Get and set server last change sequence into session storage
430
- yield self.updateLastChangeSequenceNumber()
431
- // Open socket listeners
432
- self.addSocketListeners()
433
- // request user locations
434
- const { baseURL } = self
435
- const uri = new URL('users/locations', baseURL).href
436
- const apolloFetch = self.getFetcher({
437
- locationType: 'UriLocation',
438
- uri,
439
- })
440
- yield apolloFetch(uri, {
441
- method: 'GET',
442
- signal: self.controller.signal,
443
- })
444
- window.addEventListener('beforeunload', () => {
409
+ .volatile(() => ({ roleNotificationSent: false }))
410
+ .actions((self) => {
411
+ function beforeUnloadListener() {
412
+ self.postUserLocation([])
413
+ }
414
+ function visibilityChangeListener() {
415
+ // fires when user switches tabs, apps, goes to homescreen, etc.
416
+ if (document.visibilityState === 'hidden') {
445
417
  self.postUserLocation([])
446
- })
447
- document.addEventListener('visibilitychange', () => {
448
- // fires when user switches tabs, apps, goes to homescreen, etc.
449
- if (document.visibilityState === 'hidden') {
450
- self.postUserLocation([])
418
+ }
419
+ // fires when app transitions from prerender, user returns to the app / tab.
420
+ if (document.visibilityState === 'visible') {
421
+ const { session } = getRoot<ApolloRootModel>(self)
422
+ session.broadcastLocations()
423
+ }
424
+ }
425
+ return {
426
+ initialize: flow(function* initialize(role: Role) {
427
+ if (role === 'none') {
428
+ if (!self.roleNotificationSent) {
429
+ const { session } = getRoot<ApolloRootModel>(self)
430
+ ;(session as unknown as AbstractSessionModel).notify(
431
+ 'You have registered as an Apollo user but have not been given access. Ask your administrator to enable access for your account.',
432
+ 'warning',
433
+ )
434
+ self.roleNotificationSent = true
435
+ }
436
+ return
451
437
  }
452
- // fires when app transitions from prerender, user returns to the app / tab.
453
- if (document.visibilityState === 'visible') {
454
- const { session } = getRoot<ApolloRootModel>(self)
455
- session.broadcastLocations()
438
+ if (role === 'admin') {
439
+ const rootModel = getRoot(self)
440
+ if (isAbstractMenuManager(rootModel)) {
441
+ addTopLevelAdminMenus(rootModel)
442
+ }
456
443
  }
457
- })
458
- }),
459
- }))
444
+ // Get and set server last change sequence into session storage
445
+ yield self.updateLastChangeSequenceNumber()
446
+ // Open socket listeners
447
+ self.addSocketListeners()
448
+ // request user locations
449
+ const { baseURL } = self
450
+ const uri = new URL('users/locations', baseURL).href
451
+ const apolloFetch = self.getFetcher({
452
+ locationType: 'UriLocation',
453
+ uri,
454
+ })
455
+ yield apolloFetch(uri, {
456
+ method: 'GET',
457
+ signal: self.controller.signal,
458
+ })
459
+ window.addEventListener('beforeunload', beforeUnloadListener)
460
+ document.addEventListener(
461
+ 'visibilitychange',
462
+ visibilityChangeListener,
463
+ )
464
+ }),
465
+ removeBeforeUnloadListener() {
466
+ window.removeEventListener('beforeunload', beforeUnloadListener)
467
+ },
468
+ removeVisibilityChangeListener() {
469
+ document.removeEventListener(
470
+ 'visibilitychange',
471
+ visibilityChangeListener,
472
+ )
473
+ },
474
+ }
475
+ })
460
476
  .actions((self) => ({
461
477
  afterAttach() {
462
478
  self.setRole()
@@ -481,6 +497,8 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
481
497
  )
482
498
  },
483
499
  beforeDestroy() {
500
+ self.removeBeforeUnloadListener()
501
+ self.removeVisibilityChangeListener()
484
502
  self.controller.abort('internet account beforeDestroy')
485
503
  self.socket.close()
486
504
  },
@@ -6,7 +6,6 @@ import {
6
6
  import type RpcServer from 'librpc-web-mod/dist/server'
7
7
  import { nanoid } from 'nanoid'
8
8
 
9
- import { type BackendDriver } from '../BackendDrivers'
10
9
  import { type ApolloSessionModel } from '../session'
11
10
 
12
11
  import { type RefNameAliases } from './../BackendDrivers/BackendDriver'
@@ -50,9 +49,10 @@ export default class RefNameAliasAdapter
50
49
  if (!dataStore) {
51
50
  throw new Error('No Apollo data store found')
52
51
  }
53
- const backendDriver = dataStore.getBackendDriver(
54
- assemblyId,
55
- ) as BackendDriver
52
+ const backendDriver = dataStore.getBackendDriver(assemblyId)
53
+ if (!backendDriver) {
54
+ throw new Error('No backend driver found')
55
+ }
56
56
  const refNameAliases = await backendDriver.getRefNameAliases(assemblyId)
57
57
  return refNameAliases
58
58
  }
@@ -10,7 +10,6 @@ import SimpleFeature, { type Feature } from '@jbrowse/core/util/simpleFeature'
10
10
  import { type NoAssemblyRegion, type Region } from '@jbrowse/core/util/types'
11
11
  import { nanoid } from 'nanoid'
12
12
 
13
- import { type BackendDriver } from '../BackendDrivers'
14
13
  import { type ApolloSessionModel } from '../session'
15
14
 
16
15
  // declare global {
@@ -62,9 +61,10 @@ export class ApolloSequenceAdapter extends BaseSequenceAdapter {
62
61
  if (!dataStore) {
63
62
  throw new Error('No Apollo data store found')
64
63
  }
65
- const backendDriver = dataStore.getBackendDriver(
66
- assemblyId,
67
- ) as BackendDriver
64
+ const backendDriver = dataStore.getBackendDriver(assemblyId)
65
+ if (!backendDriver) {
66
+ throw new Error('No backend driver found')
67
+ }
68
68
  const regions = await backendDriver.getRegions(assemblyId)
69
69
  this.regions = regions
70
70
  return regions
@@ -124,9 +124,11 @@ export class ApolloSequenceAdapter extends BaseSequenceAdapter {
124
124
  observer.error('No Apollo data store found')
125
125
  return
126
126
  }
127
- const backendDriver = dataStore.getBackendDriver(
128
- assemblyId,
129
- ) as BackendDriver
127
+ const backendDriver = dataStore.getBackendDriver(assemblyId)
128
+ if (!backendDriver) {
129
+ observer.error('No backend driver found')
130
+ return
131
+ }
130
132
  const regions = await backendDriver.getRegions(
131
133
  regionWithAssemblyName.assemblyName,
132
134
  )
@@ -38,6 +38,14 @@ export interface ApolloRefSeqResponse {
38
38
  assembly: string
39
39
  }
40
40
 
41
+ interface RefSeq {
42
+ refName: string
43
+ id: string
44
+ aliases: string[]
45
+ }
46
+
47
+ type RefSeqMap = Map<string, RefSeq>
48
+
41
49
  export interface ApolloInternetAccount extends BaseInternetAccountModel {
42
50
  baseURL: string
43
51
  socket: Socket
@@ -48,6 +56,8 @@ export interface ApolloInternetAccount extends BaseInternetAccountModel {
48
56
  export class CollaborationServerDriver extends BackendDriver {
49
57
  private inFlight = new Map<string, Promise<string>>()
50
58
 
59
+ private refSeqMaps = new Map<string, RefSeqMap>()
60
+
51
61
  private async fetch(
52
62
  internetAccount: ApolloInternetAccount,
53
63
  info: RequestInfo,
@@ -97,13 +107,12 @@ export class CollaborationServerDriver extends BackendDriver {
97
107
  if (!assembly) {
98
108
  throw new Error(`Could not find assembly with name "${assemblyName}"`)
99
109
  }
100
- const { ids } = getConf(assembly, ['sequence', 'metadata']) as {
101
- ids: Record<string, string>
102
- }
103
- const refSeq = ids[refName]
104
- if (!refSeq) {
110
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
111
+ const refSeqEntry = refSeqMap.get(refName)
112
+ if (!refSeqEntry) {
105
113
  throw new Error(`Could not find refSeq "${refName}"`)
106
114
  }
115
+ const refSeq = refSeqEntry.id
107
116
  const internetAccount = this.clientStore.getInternetAccount(
108
117
  assemblyName,
109
118
  ) as ApolloInternetAccount
@@ -192,13 +201,12 @@ export class CollaborationServerDriver extends BackendDriver {
192
201
  if (!assembly) {
193
202
  throw new Error(`Could not find assembly with name "${assemblyName}"`)
194
203
  }
195
- const { ids } = getConf(assembly, ['sequence', 'metadata']) as {
196
- ids: Record<string, string>
197
- }
198
- const refSeq = ids[refName]
199
- if (!refSeq) {
204
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
205
+ const refSeqEntry = refSeqMap.get(refName)
206
+ if (!refSeqEntry) {
200
207
  throw new Error(`Could not find refSeq "${refName}"`)
201
208
  }
209
+ const refSeq = refSeqEntry.id
202
210
  if (inFlightPromise) {
203
211
  const seq = await inFlightPromise
204
212
  return { seq, refSeq }
@@ -269,7 +277,11 @@ export class CollaborationServerDriver extends BackendDriver {
269
277
  return seq
270
278
  }
271
279
 
272
- async getRefNameAliases(assemblyName: string): Promise<RefNameAliases[]> {
280
+ async getRefSeqMapping(assemblyName: string): Promise<RefSeqMap> {
281
+ const cachedRefSeqMap = this.refSeqMaps.get(assemblyName)
282
+ if (cachedRefSeqMap) {
283
+ return cachedRefSeqMap
284
+ }
273
285
  const { assemblyManager } = getSession(this.clientStore)
274
286
  const assembly = assemblyManager.get(assemblyName)
275
287
  if (!assembly) {
@@ -299,13 +311,32 @@ export class CollaborationServerDriver extends BackendDriver {
299
311
  )
300
312
  }
301
313
  const refSeqs = (await response.json()) as ApolloRefSeqResponse[]
302
- return refSeqs.map((refSeq) => {
303
- return {
304
- refName: refSeq.name,
305
- aliases: [refSeq._id, ...refSeq.aliases],
306
- uniqueId: `alias-${refSeq._id}`,
307
- }
308
- }) as RefNameAliases[]
314
+ const refSeqMap = new Map<string, RefSeq>(
315
+ refSeqs.map((refSeq) => [
316
+ refSeq.name,
317
+ { refName: refSeq.name, id: refSeq._id, aliases: refSeq.aliases },
318
+ ]),
319
+ )
320
+ this.refSeqMaps.set(assemblyName, refSeqMap)
321
+ return refSeqMap
322
+ }
323
+
324
+ async getRefNameAliases(assemblyName: string): Promise<RefNameAliases[]> {
325
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
326
+ return [...refSeqMap.values()].map((refSeq) => ({
327
+ refName: refSeq.refName,
328
+ aliases: [...new Set([refSeq.id, ...refSeq.aliases])],
329
+ uniqueId: `alias-${refSeq.id}`,
330
+ }))
331
+ }
332
+
333
+ async getRefSeqId(assemblyName: string, refName: string) {
334
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
335
+ if (!refSeqMap) {
336
+ return
337
+ }
338
+ const refSeq = refSeqMap.get(refName)
339
+ return refSeq?.id
309
340
  }
310
341
 
311
342
  async getRegions(assemblyName: string): Promise<Region[]> {
@@ -13,7 +13,7 @@ import {
13
13
  annotationFeatureToGFF3,
14
14
  splitStringIntoChunks,
15
15
  } from '@apollo-annotation/shared'
16
- import gff, { type GFF3Item } from '@gmod/gff'
16
+ import { type GFF3Item, formatSync } from '@gmod/gff'
17
17
  import { getConf } from '@jbrowse/core/configuration'
18
18
  import { type Region, getSession } from '@jbrowse/core/util'
19
19
  import { getSnapshot } from 'mobx-state-tree'
@@ -165,7 +165,7 @@ export class DesktopFileDriver extends BackendDriver {
165
165
  })
166
166
  }
167
167
 
168
- const gff3Contents = gff.formatSync(gff3Items)
168
+ const gff3Contents = formatSync(gff3Items)
169
169
 
170
170
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
171
171
  const fs = require('node:fs') as typeof import('fs')
@@ -106,7 +106,9 @@ export class ChangeManager {
106
106
  // submit to driver
107
107
  const { collaborationServerDriver, getBackendDriver } = this.dataStore
108
108
  const backendDriver = isAssemblySpecificChange(change)
109
- ? getBackendDriver(change.assembly)
109
+ ? // for assembly-specific change, fall back in case it's an
110
+ // add-assembly change, since that won't exist in the driver yet
111
+ getBackendDriver(change.assembly) ?? collaborationServerDriver
110
112
  : collaborationServerDriver
111
113
  let backendResult: ValidationResultSet
112
114
  try {
@@ -66,7 +66,7 @@ export const BasicInformation = observer(function BasicInformation({
66
66
  return changeManager.submit(change)
67
67
  }
68
68
 
69
- function handleStartChange(newStart: number) {
69
+ function handleStartChange(newStart: number): boolean {
70
70
  newStart--
71
71
  const change = new LocationStartChange({
72
72
  typeName: 'LocationStartChange',
@@ -76,10 +76,11 @@ export const BasicInformation = observer(function BasicInformation({
76
76
  newStart,
77
77
  assembly,
78
78
  })
79
- return changeManager.submit(change)
79
+ void changeManager.submit(change)
80
+ return true
80
81
  }
81
82
 
82
- function handleEndChange(newEnd: number) {
83
+ function handleEndChange(newEnd: number): boolean {
83
84
  const change = new LocationEndChange({
84
85
  typeName: 'LocationEndChange',
85
86
  changedIds: [_id],
@@ -88,7 +89,8 @@ export const BasicInformation = observer(function BasicInformation({
88
89
  newEnd,
89
90
  assembly,
90
91
  })
91
- return changeManager.submit(change)
92
+ void changeManager.submit(change)
93
+ return true
92
94
  }
93
95
 
94
96
  async function fetchValidTerms(
@@ -15,7 +15,7 @@ interface NumberTextFieldProps
15
15
  | 'error'
16
16
  | 'helperText'
17
17
  > {
18
- onChangeCommitted(newValue: number): void
18
+ onChangeCommitted(newValue: number): boolean
19
19
  value: unknown
20
20
  }
21
21
 
@@ -65,7 +65,10 @@ export const NumberTextField = observer(function NumberTextField({
65
65
  if (Number.isNaN(valueAsNumber)) {
66
66
  setValue(String(initialValue))
67
67
  } else {
68
- onChangeCommitted(valueAsNumber)
68
+ const success = onChangeCommitted(valueAsNumber)
69
+ if (!success) {
70
+ setValue(String(initialValue))
71
+ }
69
72
  }
70
73
  }
71
74
  }}