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

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 (54) hide show
  1. package/dist/index.esm.js +10914 -10799
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +10979 -10865
  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 +8799 -11326
  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/components/AuthTypeSelector.tsx +1 -1
  13. package/src/ApolloInternetAccount/model.ts +87 -65
  14. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  15. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  16. package/src/BackendDrivers/CollaborationServerDriver.ts +60 -23
  17. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  18. package/src/ChangeManager.ts +22 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +68 -212
  22. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  23. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -102
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +33 -232
  25. package/src/LinearApolloDisplay/glyphs/util.ts +36 -0
  26. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +174 -0
  27. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +200 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +62 -386
  29. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +6 -0
  30. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +122 -70
  31. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
  33. package/src/components/AddAssembly.tsx +34 -38
  34. package/src/components/AddFeature.tsx +21 -18
  35. package/src/components/AddRefSeqAliases.tsx +56 -42
  36. package/src/components/CopyFeature.tsx +1 -1
  37. package/src/components/CreateApolloAnnotation.tsx +22 -10
  38. package/src/components/DeleteAssembly.tsx +2 -9
  39. package/src/components/DownloadGFF3.tsx +2 -2
  40. package/src/components/ImportFeatures.tsx +1 -1
  41. package/src/components/ManageChecks.tsx +2 -9
  42. package/src/components/ManageUsers.tsx +23 -22
  43. package/src/components/OntologyTermAutocomplete.tsx +3 -10
  44. package/src/components/OntologyTermMultiSelect.tsx +2 -2
  45. package/src/components/ViewChangeLog.tsx +25 -50
  46. package/src/components/ViewCheckResults.tsx +1 -7
  47. package/src/config.ts +3 -3
  48. package/src/index.ts +17 -16
  49. package/src/makeDisplayComponent.tsx +9 -13
  50. package/src/session/ClientDataStore.ts +33 -15
  51. package/src/session/session.ts +23 -27
  52. package/src/util/displayUtils.ts +28 -0
  53. package/src/util/glyphUtils.ts +196 -1
  54. 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.10",
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.10",
52
+ "@apollo-annotation/mst": "^0.3.10",
53
+ "@apollo-annotation/shared": "^0.3.10",
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",
@@ -57,7 +57,7 @@ export const AuthTypeSelector = ({
57
57
  }
58
58
  })
59
59
  return () => {
60
- controller.abort()
60
+ controller.abort('AuthTypeSelector')
61
61
  }
62
62
  }, [baseURL])
63
63
 
@@ -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()
@@ -473,14 +489,20 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
473
489
  return
474
490
  }
475
491
  if (self.role) {
476
- await self.initialize(self.role)
477
- reaction.dispose()
492
+ try {
493
+ await self.initialize(self.role)
494
+ reaction.dispose()
495
+ } catch {
496
+ // if initialize fails, do nothing so the autorun runs again
497
+ }
478
498
  }
479
499
  },
480
500
  { name: 'ApolloInternetAccount' },
481
501
  )
482
502
  },
483
503
  beforeDestroy() {
504
+ self.removeBeforeUnloadListener()
505
+ self.removeVisibilityChangeListener()
484
506
  self.controller.abort('internet account beforeDestroy')
485
507
  self.socket.close()
486
508
  },
@@ -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
  )
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  type ChangeMessage,
20
20
  ValidationResultSet,
21
+ makeUserSessionId,
21
22
  } from '@apollo-annotation/shared'
22
23
  import { getConf } from '@jbrowse/core/configuration'
23
24
  import { type BaseInternetAccountModel } from '@jbrowse/core/pluggableElementTypes'
@@ -38,6 +39,14 @@ export interface ApolloRefSeqResponse {
38
39
  assembly: string
39
40
  }
40
41
 
42
+ interface RefSeq {
43
+ refName: string
44
+ id: string
45
+ aliases: string[]
46
+ }
47
+
48
+ type RefSeqMap = Map<string, RefSeq>
49
+
41
50
  export interface ApolloInternetAccount extends BaseInternetAccountModel {
42
51
  baseURL: string
43
52
  socket: Socket
@@ -48,6 +57,8 @@ export interface ApolloInternetAccount extends BaseInternetAccountModel {
48
57
  export class CollaborationServerDriver extends BackendDriver {
49
58
  private inFlight = new Map<string, Promise<string>>()
50
59
 
60
+ private refSeqMaps = new Map<string, RefSeqMap>()
61
+
51
62
  private async fetch(
52
63
  internetAccount: ApolloInternetAccount,
53
64
  info: RequestInfo,
@@ -97,13 +108,12 @@ export class CollaborationServerDriver extends BackendDriver {
97
108
  if (!assembly) {
98
109
  throw new Error(`Could not find assembly with name "${assemblyName}"`)
99
110
  }
100
- const { ids } = getConf(assembly, ['sequence', 'metadata']) as {
101
- ids: Record<string, string>
102
- }
103
- const refSeq = ids[refName]
104
- if (!refSeq) {
111
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
112
+ const refSeqEntry = refSeqMap.get(refName)
113
+ if (!refSeqEntry) {
105
114
  throw new Error(`Could not find refSeq "${refName}"`)
106
115
  }
116
+ const refSeq = refSeqEntry.id
107
117
  const internetAccount = this.clientStore.getInternetAccount(
108
118
  assemblyName,
109
119
  ) as ApolloInternetAccount
@@ -145,6 +155,10 @@ export class CollaborationServerDriver extends BackendDriver {
145
155
  ) {
146
156
  const { socket } = internetAccount
147
157
  const token = internetAccount.retrieveToken()
158
+ if (!token) {
159
+ return
160
+ }
161
+ const localSessionId = makeUserSessionId(token)
148
162
  const channel = `${assembly}-${refSeq}`
149
163
  const changeManager = new ChangeManager(this.clientStore)
150
164
 
@@ -154,11 +168,12 @@ export class CollaborationServerDriver extends BackendDriver {
154
168
  internetAccount.setLastChangeSequenceNumber(
155
169
  Number(message.changeSequence),
156
170
  )
157
- if (message.userSessionId !== token && message.channel === channel) {
158
- const change = Change.fromJSON(message.changeInfo)
159
- if (isFeatureChange(change) && this.haveDataForChange(change)) {
160
- await changeManager.submit(change, { submitToBackend: false })
161
- }
171
+ if (message.userSessionId === localSessionId) {
172
+ return // we did this change, no need to apply it again
173
+ }
174
+ const change = Change.fromJSON(message.changeInfo)
175
+ if (isFeatureChange(change) && this.haveDataForChange(change)) {
176
+ await changeManager.submit(change, { submitToBackend: false })
162
177
  }
163
178
  })
164
179
  }
@@ -192,13 +207,12 @@ export class CollaborationServerDriver extends BackendDriver {
192
207
  if (!assembly) {
193
208
  throw new Error(`Could not find assembly with name "${assemblyName}"`)
194
209
  }
195
- const { ids } = getConf(assembly, ['sequence', 'metadata']) as {
196
- ids: Record<string, string>
197
- }
198
- const refSeq = ids[refName]
199
- if (!refSeq) {
210
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
211
+ const refSeqEntry = refSeqMap.get(refName)
212
+ if (!refSeqEntry) {
200
213
  throw new Error(`Could not find refSeq "${refName}"`)
201
214
  }
215
+ const refSeq = refSeqEntry.id
202
216
  if (inFlightPromise) {
203
217
  const seq = await inFlightPromise
204
218
  return { seq, refSeq }
@@ -269,7 +283,11 @@ export class CollaborationServerDriver extends BackendDriver {
269
283
  return seq
270
284
  }
271
285
 
272
- async getRefNameAliases(assemblyName: string): Promise<RefNameAliases[]> {
286
+ async getRefSeqMapping(assemblyName: string): Promise<RefSeqMap> {
287
+ const cachedRefSeqMap = this.refSeqMaps.get(assemblyName)
288
+ if (cachedRefSeqMap) {
289
+ return cachedRefSeqMap
290
+ }
273
291
  const { assemblyManager } = getSession(this.clientStore)
274
292
  const assembly = assemblyManager.get(assemblyName)
275
293
  if (!assembly) {
@@ -299,13 +317,32 @@ export class CollaborationServerDriver extends BackendDriver {
299
317
  )
300
318
  }
301
319
  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[]
320
+ const refSeqMap = new Map<string, RefSeq>(
321
+ refSeqs.map((refSeq) => [
322
+ refSeq.name,
323
+ { refName: refSeq.name, id: refSeq._id, aliases: refSeq.aliases },
324
+ ]),
325
+ )
326
+ this.refSeqMaps.set(assemblyName, refSeqMap)
327
+ return refSeqMap
328
+ }
329
+
330
+ async getRefNameAliases(assemblyName: string): Promise<RefNameAliases[]> {
331
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
332
+ return [...refSeqMap.values()].map((refSeq) => ({
333
+ refName: refSeq.refName,
334
+ aliases: [...new Set([refSeq.id, ...refSeq.aliases])],
335
+ uniqueId: `alias-${refSeq.id}`,
336
+ }))
337
+ }
338
+
339
+ async getRefSeqId(assemblyName: string, refName: string) {
340
+ const refSeqMap = await this.getRefSeqMapping(assemblyName)
341
+ if (!refSeqMap) {
342
+ return
343
+ }
344
+ const refSeq = refSeqMap.get(refName)
345
+ return refSeq?.id
309
346
  }
310
347
 
311
348
  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')
@@ -42,21 +42,31 @@ export class ChangeManager {
42
42
  const session = getSession(this.dataStore)
43
43
  const controller = new AbortController()
44
44
 
45
- const { jobsManager, isLocked } = getSession(
46
- this.dataStore,
47
- ) as unknown as ApolloSessionModel
45
+ // eslint-disable-next-line @typescript-eslint/unbound-method
46
+ const { jobsManager, isLocked, changeInProgress, setChangeInProgress } =
47
+ getSession(this.dataStore) as unknown as ApolloSessionModel
48
48
 
49
49
  if (isLocked) {
50
50
  session.notify('Cannot submit changes in locked mode')
51
+ setChangeInProgress(false)
51
52
  return
52
53
  }
53
54
 
55
+ if (changeInProgress) {
56
+ session.notify(
57
+ 'Could not submit change, there is another change still in progress',
58
+ )
59
+ return
60
+ }
61
+
62
+ setChangeInProgress(true)
63
+
54
64
  const job = {
55
65
  name: change.typeName,
56
66
  statusMessage: 'Pre-validating',
57
67
  progressPct: 0,
58
68
  cancelCallback: () => {
59
- controller.abort()
69
+ controller.abort('ChangeManager')
60
70
  },
61
71
  }
62
72
 
@@ -71,6 +81,7 @@ export class ChangeManager {
71
81
  jobsManager.abortJob(job.name, msg)
72
82
  }
73
83
  session.notify(msg, 'error')
84
+ setChangeInProgress(false)
74
85
  return
75
86
  }
76
87
 
@@ -86,6 +97,7 @@ export class ChangeManager {
86
97
  `Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`,
87
98
  'error',
88
99
  )
100
+ setChangeInProgress(false)
89
101
  return
90
102
  }
91
103
 
@@ -106,7 +118,9 @@ export class ChangeManager {
106
118
  // submit to driver
107
119
  const { collaborationServerDriver, getBackendDriver } = this.dataStore
108
120
  const backendDriver = isAssemblySpecificChange(change)
109
- ? getBackendDriver(change.assembly)
121
+ ? // for assembly-specific change, fall back in case it's an
122
+ // add-assembly change, since that won't exist in the driver yet
123
+ getBackendDriver(change.assembly) ?? collaborationServerDriver
110
124
  : collaborationServerDriver
111
125
  let backendResult: ValidationResultSet
112
126
  try {
@@ -118,6 +132,7 @@ export class ChangeManager {
118
132
  console.error(error)
119
133
  session.notify(String(error), 'error')
120
134
  await this.undo(change, false)
135
+ setChangeInProgress(false)
121
136
  return
122
137
  }
123
138
  if (!backendResult.ok) {
@@ -127,6 +142,7 @@ export class ChangeManager {
127
142
  }
128
143
  session.notify(msg, 'error')
129
144
  await this.undo(change, false)
145
+ setChangeInProgress(false)
130
146
  return
131
147
  }
132
148
  if (change.notification) {
@@ -141,6 +157,7 @@ export class ChangeManager {
141
157
  if (updateJobsManager) {
142
158
  jobsManager.done(job)
143
159
  }
160
+ setChangeInProgress(false)
144
161
  }
145
162
 
146
163
  async undo(change: Change, submitToBackend = true) {
@@ -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
  }}