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

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 (84) hide show
  1. package/dist/index.esm.js +4603 -2045
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
  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 +9387 -4016
  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 +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +42 -18
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
  18. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
  19. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  20. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
  21. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
  22. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
  23. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
  24. package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
  25. package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
  26. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  27. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
  28. package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
  29. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  34. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  36. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
  37. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  38. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
  39. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  40. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
  41. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
  42. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
  43. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
  44. package/src/OntologyManager/index.ts +4 -1
  45. package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
  46. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  47. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  48. package/src/components/AddAssembly.tsx +1 -1
  49. package/src/components/AddAssemblyAliases.tsx +114 -0
  50. package/src/components/AddChildFeature.tsx +7 -7
  51. package/src/components/AddFeature.tsx +20 -15
  52. package/src/components/AddRefSeqAliases.tsx +9 -9
  53. package/src/components/CopyFeature.tsx +4 -4
  54. package/src/components/CreateApolloAnnotation.tsx +335 -151
  55. package/src/components/DeleteAssembly.tsx +1 -1
  56. package/src/components/DeleteFeature.tsx +358 -11
  57. package/src/components/DownloadGFF3.tsx +20 -1
  58. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  59. package/src/components/FilterFeatures.tsx +7 -7
  60. package/src/components/FilterTranscripts.tsx +86 -0
  61. package/src/components/ImportFeatures.tsx +1 -1
  62. package/src/components/ManageChecks.tsx +1 -1
  63. package/src/components/MergeExons.tsx +193 -0
  64. package/src/components/MergeTranscripts.tsx +182 -0
  65. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  66. package/src/components/OpenLocalFile.tsx +11 -7
  67. package/src/components/SplitExon.tsx +134 -0
  68. package/src/components/ViewCheckResults.tsx +1 -1
  69. package/src/components/index.ts +4 -0
  70. package/src/config.ts +11 -0
  71. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  72. package/src/extensions/annotationFromPileup.ts +99 -89
  73. package/src/index.ts +42 -105
  74. package/src/makeDisplayComponent.tsx +0 -1
  75. package/src/menus/index.ts +1 -0
  76. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
  77. package/src/menus/topLevelMenuAdmin.ts +154 -0
  78. package/src/session/session.ts +163 -104
  79. package/src/util/annotationFeatureUtils.ts +59 -0
  80. package/src/util/copyToClipboard.ts +21 -0
  81. package/src/util/displayUtils.ts +149 -0
  82. package/src/util/glyphUtils.ts +201 -0
  83. package/src/util/index.ts +2 -0
  84. package/src/util/mouseEventsUtils.ts +145 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/jbrowse-plugin-apollo",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Apollo plugin for JBrowse 2",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -48,15 +48,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@apollo-annotation/common": "^0.3.6",
52
- "@apollo-annotation/mst": "^0.3.6",
53
- "@apollo-annotation/shared": "^0.3.6",
51
+ "@apollo-annotation/common": "^0.3.8",
52
+ "@apollo-annotation/mst": "^0.3.8",
53
+ "@apollo-annotation/shared": "^0.3.8",
54
54
  "@emotion/react": "^11.10.6",
55
55
  "@emotion/styled": "^11.10.6",
56
56
  "@gmod/gff": "1.2.0",
57
- "@jbrowse/plugin-authentication": "^3.0.1",
58
- "@jbrowse/plugin-linear-genome-view": "^3.0.1",
59
- "@mui/icons-material": "^5.8.4",
57
+ "@jbrowse/plugin-authentication": "^3.6.5",
58
+ "@jbrowse/plugin-linear-genome-view": "^3.6.5",
59
+ "@mui/icons-material": "^6.5.0",
60
60
  "@types/jsonpath": "^0.2.0",
61
61
  "autosuggest-highlight": "^3.3.4",
62
62
  "bson-objectid": "^2.0.4",
@@ -71,12 +71,12 @@
71
71
  "tslib": "^2.3.1"
72
72
  },
73
73
  "devDependencies": {
74
- "@jbrowse/cli": "^3.0.1",
75
- "@jbrowse/core": "^3.0.1",
76
- "@jbrowse/development-tools": "^2.1.1",
74
+ "@jbrowse/cli": "^3.6.5",
75
+ "@jbrowse/core": "^3.6.5",
76
+ "@jbrowse/development-tools": "^2.2.1",
77
77
  "@jest/globals": "^29.0.3",
78
- "@mui/material": "^6.0.0",
79
- "@mui/x-data-grid": "^7.0.0",
78
+ "@mui/material": "^7.0.0",
79
+ "@mui/x-data-grid": "^8.0.0",
80
80
  "@types/autosuggest-highlight": "^3",
81
81
  "@types/file-saver": "^2",
82
82
  "@types/node": "^18.14.2",
@@ -99,7 +99,7 @@
99
99
  "react": "^18.2.0",
100
100
  "react-dom": "^18.2.0",
101
101
  "rimraf": "^3.0.2",
102
- "rollup": "^2.59.0",
102
+ "rollup": "^2.79.2",
103
103
  "rxjs": "^7.4.0",
104
104
  "serve": "^14.0.1",
105
105
  "shx": "^0.3.3",
@@ -111,9 +111,9 @@
111
111
  },
112
112
  "peerDependencies": {
113
113
  "@jbrowse/core": "^3.0.1",
114
- "@mui/material": "^6.0.0",
114
+ "@mui/material": "^7.0.0",
115
115
  "mobx": "^6.6.1",
116
- "mobx-react": "^7.2.1",
116
+ "mobx-react": "^9.0.0",
117
117
  "mobx-state-tree": "^5.4.0",
118
118
  "prop-types": "^15.8.1",
119
119
  "react": "^18.2.0",
@@ -23,14 +23,14 @@ import {
23
23
  isElectron,
24
24
  } from '@jbrowse/core/util'
25
25
  import { autorun } from 'mobx'
26
- import { type Instance, flow, getRoot, types } from 'mobx-state-tree'
26
+ import { type Instance, flow, getRoot, isAlive, types } from 'mobx-state-tree'
27
27
  import { io } from 'socket.io-client'
28
28
 
29
+ import { addTopLevelAdminMenus } from '../menus/topLevelMenuAdmin'
29
30
  import { type Collaborator } from '../session'
30
31
  import { type ApolloRootModel } from '../types'
31
32
  import { createFetchErrorMessage } from '../util'
32
33
 
33
- import { addMenuItems } from './addMenuItems'
34
34
  import { AuthTypeSelector } from './components/AuthTypeSelector'
35
35
  import { type ApolloInternetAccountConfigModel } from './configSchema'
36
36
 
@@ -61,7 +61,9 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
61
61
  }))
62
62
  .volatile(() => ({
63
63
  role: undefined as Role | undefined,
64
+ controller: new AbortController(),
64
65
  }))
66
+
65
67
  .actions((self) => {
66
68
  let roleNotificationSent = false
67
69
  return {
@@ -205,7 +207,7 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
205
207
  const searchParams = new URLSearchParams({ type: authType })
206
208
  url.search = searchParams.toString()
207
209
  const uri = url.toString()
208
- const response = await fetch(uri)
210
+ const response = await fetch(uri, { signal: self.controller.signal })
209
211
  if (!response.ok) {
210
212
  const errorMessage = await createFetchErrorMessage(
211
213
  response,
@@ -239,7 +241,18 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
239
241
  uri,
240
242
  })
241
243
 
242
- const response = yield apolloFetch(uri, { method: 'GET' })
244
+ let response: Response
245
+ try {
246
+ response = yield apolloFetch(uri, {
247
+ method: 'GET',
248
+ signal: self.controller.signal,
249
+ })
250
+ } catch (error) {
251
+ if (!self.controller.signal.aborted) {
252
+ console.error(error)
253
+ }
254
+ return
255
+ }
243
256
  if (!response.ok) {
244
257
  const errorMessage = yield createFetchErrorMessage(
245
258
  response,
@@ -274,7 +287,18 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
274
287
  uri,
275
288
  })
276
289
 
277
- const response = yield apolloFetch(uri, { method: 'GET' })
290
+ let response: Response
291
+ try {
292
+ response = yield apolloFetch(uri, {
293
+ method: 'GET',
294
+ signal: self.controller.signal,
295
+ })
296
+ } catch (error) {
297
+ if (!self.controller.signal.aborted) {
298
+ console.error(error)
299
+ }
300
+ return
301
+ }
278
302
  if (!response.ok) {
279
303
  console.error(
280
304
  `Error when fetching the last updates to recover socket connection — ${response.status}`,
@@ -300,11 +324,13 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
300
324
  if (!token) {
301
325
  throw new Error('No Token found')
302
326
  }
327
+ const user = getDecodedToken(token)
328
+ const localSessionId = makeUserSessionId(user)
303
329
  const { socket } = self
304
330
  const { addCheckResult, changeManager, deleteCheckResult } =
305
331
  session.apolloDataStore
306
- socket.on('connect', async () => {
307
- await self.getMissingChanges()
332
+ socket.on('connect', () => {
333
+ void self.getMissingChanges()
308
334
  })
309
335
  socket.on('connect_error', (error) => {
310
336
  console.error(error)
@@ -324,7 +350,7 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
324
350
  'LastChangeSequence',
325
351
  String(message.changeSequence),
326
352
  )
327
- if (message.userSessionId === token) {
353
+ if (message.userSessionId === localSessionId) {
328
354
  return // we did this change, no need to apply it again
329
355
  }
330
356
  const change = Change.fromJSON(message.changeInfo)
@@ -332,8 +358,6 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
332
358
  })
333
359
  socket.on('USER_LOCATION', (message: UserLocationMessage) => {
334
360
  const { channel, locations, userName, userSessionId } = message
335
- const user = getDecodedToken(token)
336
- const localSessionId = makeUserSessionId(user)
337
361
  if (channel === 'USER_LOCATION' && userSessionId !== localSessionId) {
338
362
  const collaborator: Collaborator = {
339
363
  name: userName,
@@ -356,7 +380,10 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
356
380
  }))
357
381
  .actions((self) => {
358
382
  async function postUserLocation(userLoc: UserLocation[]) {
359
- const { baseURL } = self
383
+ if (!isAlive(self)) {
384
+ return
385
+ }
386
+ const { baseURL, controller } = self
360
387
  const url = new URL('users/userLocation', baseURL).href
361
388
  const userLocation = new URLSearchParams(JSON.stringify(userLoc))
362
389
 
@@ -368,6 +395,7 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
368
395
  const response = await apolloFetch(url, {
369
396
  method: 'POST',
370
397
  body: userLocation,
398
+ signal: controller.signal,
371
399
  })
372
400
  if (!response.ok) {
373
401
  throw new Error('ignore') // ignore message, will get caught by "catch"
@@ -395,7 +423,7 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
395
423
  if (role === 'admin') {
396
424
  const rootModel = getRoot(self)
397
425
  if (isAbstractMenuManager(rootModel)) {
398
- addMenuItems(rootModel)
426
+ addTopLevelAdminMenus(rootModel)
399
427
  }
400
428
  }
401
429
  // Get and set server last change sequence into session storage
@@ -409,7 +437,10 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
409
437
  locationType: 'UriLocation',
410
438
  uri,
411
439
  })
412
- yield apolloFetch(uri, { method: 'GET' })
440
+ yield apolloFetch(uri, {
441
+ method: 'GET',
442
+ signal: self.controller.signal,
443
+ })
413
444
  window.addEventListener('beforeunload', () => {
414
445
  self.postUserLocation([])
415
446
  })
@@ -449,6 +480,10 @@ const stateModelFactory = (configSchema: ApolloInternetAccountConfigModel) => {
449
480
  { name: 'ApolloInternetAccount' },
450
481
  )
451
482
  },
483
+ beforeDestroy() {
484
+ self.controller.abort('internet account beforeDestroy')
485
+ self.socket.close()
486
+ },
452
487
  }))
453
488
  }
454
489
 
@@ -4,7 +4,12 @@
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
5
  /* eslint-disable @typescript-eslint/no-unsafe-call */
6
6
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
7
- import { type AssemblySpecificChange, Change } from '@apollo-annotation/common'
7
+ import {
8
+ type AssemblySpecificChange,
9
+ Change,
10
+ type FeatureChange,
11
+ isFeatureChange,
12
+ } from '@apollo-annotation/common'
8
13
  import {
9
14
  type AnnotationFeatureSnapshot,
10
15
  type ApolloRefSeqI,
@@ -151,12 +156,28 @@ export class CollaborationServerDriver extends BackendDriver {
151
156
  )
152
157
  if (message.userSessionId !== token && message.channel === channel) {
153
158
  const change = Change.fromJSON(message.changeInfo)
154
- await changeManager.submit(change, { submitToBackend: false })
159
+ if (isFeatureChange(change) && this.haveDataForChange(change)) {
160
+ await changeManager.submit(change, { submitToBackend: false })
161
+ }
155
162
  }
156
163
  })
157
164
  }
158
165
  }
159
166
 
167
+ private haveDataForChange(change: FeatureChange): boolean {
168
+ const { assembly, changedIds } = change
169
+ const apolloAssembly = this.clientStore.assemblies.get(assembly)
170
+ if (!apolloAssembly) {
171
+ return false
172
+ }
173
+ for (const changedId of changedIds) {
174
+ if (this.clientStore.getFeature(changedId)) {
175
+ return true
176
+ }
177
+ }
178
+ return false
179
+ }
180
+
160
181
  /**
161
182
  * Call backend endpoint to get sequence by criteria
162
183
  * @param region - Searchable region containing refSeq, start and end
@@ -30,6 +30,7 @@ export class ChangeManager {
30
30
  constructor(private dataStore: ClientDataStore & IAnyStateTreeNode) {}
31
31
 
32
32
  recentChanges: Change[] = []
33
+ undoneChanges: Change[] = []
33
34
 
34
35
  async submit(change: Change, opts: SubmitOpts = {}) {
35
36
  const {
@@ -41,10 +42,15 @@ export class ChangeManager {
41
42
  const session = getSession(this.dataStore)
42
43
  const controller = new AbortController()
43
44
 
44
- const { jobsManager } = getSession(
45
+ const { jobsManager, isLocked } = getSession(
45
46
  this.dataStore,
46
47
  ) as unknown as ApolloSessionModel
47
48
 
49
+ if (isLocked) {
50
+ session.notify('Cannot submit changes in locked mode')
51
+ return
52
+ }
53
+
48
54
  const job = {
49
55
  name: change.typeName,
50
56
  statusMessage: 'Pre-validating',
@@ -76,7 +82,10 @@ export class ChangeManager {
76
82
  jobsManager.abortJob(job.name, String(error))
77
83
  }
78
84
  console.error(error)
79
- session.notify(String(error), 'error')
85
+ session.notify(
86
+ `Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`,
87
+ 'error',
88
+ )
80
89
  return
81
90
  }
82
91
 
@@ -87,7 +96,7 @@ export class ChangeManager {
87
96
  )
88
97
  if (!results2.ok) {
89
98
  // notify of invalid change and revert
90
- await this.revert(change)
99
+ await this.undo(change)
91
100
  }
92
101
 
93
102
  if (submitToBackend) {
@@ -108,7 +117,7 @@ export class ChangeManager {
108
117
  }
109
118
  console.error(error)
110
119
  session.notify(String(error), 'error')
111
- await this.revert(change, false)
120
+ await this.undo(change, false)
112
121
  return
113
122
  }
114
123
  if (!backendResult.ok) {
@@ -117,16 +126,16 @@ export class ChangeManager {
117
126
  jobsManager.abortJob(job.name, msg)
118
127
  }
119
128
  session.notify(msg, 'error')
120
- await this.revert(change, false)
129
+ await this.undo(change, false)
121
130
  return
122
131
  }
123
132
  if (change.notification) {
124
133
  session.notify(change.notification, 'success')
125
134
  }
126
- }
127
- if (addToRecents) {
128
- // Push the change into array
129
- this.recentChanges.push(change)
135
+ if (addToRecents) {
136
+ this.recentChanges.push(change)
137
+ this.undoneChanges = []
138
+ }
130
139
  }
131
140
 
132
141
  if (updateJobsManager) {
@@ -134,21 +143,36 @@ export class ChangeManager {
134
143
  }
135
144
  }
136
145
 
137
- async revert(change: Change, submitToBackend = true) {
146
+ async undo(change: Change, submitToBackend = true) {
138
147
  const inverseChange = change.getInverse()
139
- return this.submit(inverseChange, { submitToBackend, addToRecents: false })
148
+ const opts = { submitToBackend, addToRecents: false }
149
+ return this.submit(inverseChange, opts)
150
+ }
151
+
152
+ async redo(change: Change, submitToBackend = true) {
153
+ const opts = { submitToBackend, addToRecents: false }
154
+ return this.submit(change, opts)
140
155
  }
141
156
 
142
- /**
143
- * Undo the last change
144
- */
145
- async revertLastChange() {
157
+ async undoLastChange() {
158
+ const session = getSession(this.dataStore)
146
159
  const lastChange = this.recentChanges.pop()
147
160
  if (!lastChange) {
148
- const session = getSession(this.dataStore)
149
- session.notify('No changes to undo!', 'warning')
161
+ session.notify('No changes to undo!', 'info')
162
+ return
163
+ }
164
+ this.undoneChanges.push(lastChange)
165
+ return this.undo(lastChange)
166
+ }
167
+
168
+ async redoLastChange() {
169
+ const session = getSession(this.dataStore)
170
+ const lastChange = this.undoneChanges.pop()
171
+ if (!lastChange) {
172
+ session.notify('No changes to redo!', 'info')
150
173
  return
151
174
  }
152
- return this.revert(lastChange)
175
+ this.recentChanges.push(lastChange)
176
+ return this.redo(lastChange)
153
177
  }
154
178
  }
@@ -58,7 +58,7 @@ export const ApolloTranscriptDetailsWidget = observer(
58
58
  model: ApolloTranscriptDetailsWidgetState
59
59
  }) {
60
60
  const { classes } = useStyles()
61
- const DEFAULT_PANELS = ['summary', 'location', 'attrs']
61
+ const DEFAULT_PANELS = ['summary', 'location']
62
62
  const [panelState, setPanelState] = useState<string[]>(DEFAULT_PANELS)
63
63
 
64
64
  const { model } = props
@@ -106,10 +106,53 @@ export const ApolloTranscriptDetailsWidget = observer(
106
106
  }
107
107
  }
108
108
 
109
- const CustomComponent = pluginManager.evaluateExtensionPoint(
110
- 'Apollo-TranscriptDetailsCustomComponent',
109
+ const CustomComponentInsideSummary = pluginManager.evaluateExtensionPoint(
110
+ 'Apollo-TranscriptDetailsCustomComponent-InsideSummary',
111
111
  NoOpCustomComponent,
112
- props,
112
+ { feature, session },
113
+ ) as React.ElementType<CustomComponentProps>
114
+
115
+ const CustomComponentAfterSummary = pluginManager.evaluateExtensionPoint(
116
+ 'Apollo-TranscriptDetailsCustomComponent-AfterSummary',
117
+ NoOpCustomComponent,
118
+ { feature, session },
119
+ ) as React.ElementType<CustomComponentProps>
120
+
121
+ const CustomComponentInsideLocation = pluginManager.evaluateExtensionPoint(
122
+ 'Apollo-TranscriptDetailsCustomComponent-InsideLocation',
123
+ NoOpCustomComponent,
124
+ { feature, session },
125
+ ) as React.ElementType<CustomComponentProps>
126
+
127
+ const CustomComponentAfterLocation = pluginManager.evaluateExtensionPoint(
128
+ 'Apollo-TranscriptDetailsCustomComponent-AfterLocation',
129
+ NoOpCustomComponent,
130
+ { feature, session },
131
+ ) as React.ElementType<CustomComponentProps>
132
+
133
+ const CustomComponentInsideAttributes =
134
+ pluginManager.evaluateExtensionPoint(
135
+ 'Apollo-TranscriptDetailsCustomComponent-InsideAttributes',
136
+ NoOpCustomComponent,
137
+ { feature, session },
138
+ ) as React.ElementType<CustomComponentProps>
139
+
140
+ const CustomComponentAfterAttributes = pluginManager.evaluateExtensionPoint(
141
+ 'Apollo-TranscriptDetailsCustomComponent-AfterAttributes',
142
+ NoOpCustomComponent,
143
+ { feature, session },
144
+ ) as React.ElementType<CustomComponentProps>
145
+
146
+ const CustomComponentInsideSequence = pluginManager.evaluateExtensionPoint(
147
+ 'Apollo-TranscriptDetailsCustomComponent-InsideSequence',
148
+ NoOpCustomComponent,
149
+ { feature, session },
150
+ ) as React.ElementType<CustomComponentProps>
151
+
152
+ const CustomComponentAfterSequence = pluginManager.evaluateExtensionPoint(
153
+ 'Apollo-TranscriptDetailsCustomComponent-AfterSequence',
154
+ NoOpCustomComponent,
155
+ { feature, session },
113
156
  ) as React.ElementType<CustomComponentProps>
114
157
 
115
158
  return (
@@ -131,9 +174,10 @@ export const ApolloTranscriptDetailsWidget = observer(
131
174
  </StyledAccordionSummary>
132
175
  <AccordionDetails>
133
176
  <TranscriptWidgetSummary feature={feature} refName={refName} />
177
+ <CustomComponentInsideSummary session={session} feature={feature} />
134
178
  </AccordionDetails>
135
179
  </Accordion>
136
- <CustomComponent session={session} feature={feature} />
180
+ <CustomComponentAfterSummary session={session} feature={feature} />
137
181
  <Accordion
138
182
  style={{ marginTop: 5 }}
139
183
  expanded={panelState.includes('location')}
@@ -157,8 +201,13 @@ export const ApolloTranscriptDetailsWidget = observer(
157
201
  session={apolloSession}
158
202
  assembly={currentAssembly._id || ''}
159
203
  />
204
+ <CustomComponentInsideLocation
205
+ session={session}
206
+ feature={feature}
207
+ />
160
208
  </AccordionDetails>
161
209
  </Accordion>
210
+ <CustomComponentAfterLocation session={session} feature={feature} />
162
211
  <Accordion
163
212
  style={{ marginTop: 5 }}
164
213
  expanded={panelState.includes('attrs')}
@@ -189,8 +238,13 @@ export const ApolloTranscriptDetailsWidget = observer(
189
238
  assembly={currentAssembly._id || ''}
190
239
  editable={editable}
191
240
  />
241
+ <CustomComponentInsideAttributes
242
+ session={session}
243
+ feature={feature}
244
+ />
192
245
  </AccordionDetails>
193
246
  </Accordion>
247
+ <CustomComponentAfterAttributes session={session} feature={feature} />
194
248
  <Accordion
195
249
  style={{ marginTop: 5 }}
196
250
  expanded={panelState.includes('sequence')}
@@ -216,8 +270,13 @@ export const ApolloTranscriptDetailsWidget = observer(
216
270
  refName={refName}
217
271
  />
218
272
  )}
273
+ <CustomComponentInsideSequence
274
+ session={session}
275
+ feature={feature}
276
+ />
219
277
  </AccordionDetails>
220
278
  </Accordion>
279
+ <CustomComponentAfterSequence feature={feature} session={session} />
221
280
  </div>
222
281
  )
223
282
  },
@@ -105,13 +105,15 @@ export const Attributes = observer(function Attributes({
105
105
  typeName: 'FeatureAttributeChange',
106
106
  assembly,
107
107
  featureId: _id,
108
- attributes: remainingAttributes,
108
+ oldAttributes: attributesSerialized,
109
+ newAttributes: remainingAttributes,
109
110
  })
110
111
  void changeManager.submit(change)
111
112
  }
112
113
 
113
114
  function modifyFeatureAttribute(key: string, attribute: string[]) {
114
115
  const serializedAttributes = { ...getSnapshot(attributes) }
116
+ const oldAttributes = structuredClone(serializedAttributes)
115
117
  if (!(key in serializedAttributes)) {
116
118
  notify(`"${key}" not found in feature attributes`, 'error')
117
119
  return
@@ -127,13 +129,15 @@ export const Attributes = observer(function Attributes({
127
129
  typeName: 'FeatureAttributeChange',
128
130
  assembly,
129
131
  featureId: feature._id,
130
- attributes: serializedAttributes,
132
+ oldAttributes,
133
+ newAttributes: serializedAttributes,
131
134
  })
132
135
  void changeManager.submit(change)
133
136
  }
134
137
 
135
138
  function addFeatureAttribute(key: string, attribute: string[]) {
136
139
  const serializedAttributes = { ...getSnapshot(attributes) }
140
+ const oldAttributes = structuredClone(serializedAttributes)
137
141
  if (key in serializedAttributes) {
138
142
  notify(`Feature already has attribute "${key}"`, 'error')
139
143
  return
@@ -145,7 +149,8 @@ export const Attributes = observer(function Attributes({
145
149
  typeName: 'FeatureAttributeChange',
146
150
  assembly,
147
151
  featureId: feature._id,
148
- attributes: serializedAttributes,
152
+ oldAttributes,
153
+ newAttributes: serializedAttributes,
149
154
  })
150
155
  void changeManager.submit(change)
151
156
  }