@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 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 (86) hide show
  1. package/dist/index.esm.js +11212 -10483
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +11251 -10509
  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 +7726 -9014
  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 +18 -18
  12. package/src/ApolloInternetAccount/model.ts +123 -70
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +72 -20
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +36 -14
  18. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  22. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +72 -234
  23. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +23 -131
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +279 -217
  27. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  28. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  29. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  30. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  31. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  32. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  33. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  34. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  36. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  37. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  38. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  39. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  40. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  41. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  42. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +157 -0
  43. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +101 -38
  44. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +334 -262
  45. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  46. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  47. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  48. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  49. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  50. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  51. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  52. package/src/components/AddAssembly.tsx +34 -38
  53. package/src/components/AddAssemblyAliases.tsx +1 -1
  54. package/src/components/AddChildFeature.tsx +5 -2
  55. package/src/components/AddFeature.tsx +30 -21
  56. package/src/components/AddRefSeqAliases.tsx +64 -50
  57. package/src/components/CopyFeature.tsx +4 -2
  58. package/src/components/CreateApolloAnnotation.tsx +22 -9
  59. package/src/components/DeleteAssembly.tsx +3 -10
  60. package/src/components/DownloadGFF3.tsx +2 -2
  61. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  62. package/src/components/FilterFeatures.tsx +7 -7
  63. package/src/components/FilterTranscripts.tsx +6 -6
  64. package/src/components/ImportFeatures.tsx +1 -1
  65. package/src/components/ManageChecks.tsx +3 -10
  66. package/src/components/ManageUsers.tsx +23 -22
  67. package/src/components/MergeTranscripts.tsx +12 -15
  68. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  69. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  70. package/src/components/OpenLocalFile.tsx +11 -7
  71. package/src/components/ViewChangeLog.tsx +25 -50
  72. package/src/components/ViewCheckResults.tsx +2 -8
  73. package/src/components/index.ts +1 -0
  74. package/src/config.ts +6 -0
  75. package/src/index.ts +53 -115
  76. package/src/makeDisplayComponent.tsx +9 -14
  77. package/src/menus/index.ts +1 -0
  78. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  79. package/src/menus/topLevelMenuAdmin.ts +154 -0
  80. package/src/session/ClientDataStore.ts +32 -14
  81. package/src/session/session.ts +159 -121
  82. package/src/util/annotationFeatureUtils.ts +15 -21
  83. package/src/util/displayUtils.ts +149 -0
  84. package/src/util/glyphUtils.ts +329 -0
  85. package/src/util/loadAssemblyIntoClient.ts +3 -2
  86. package/src/util/mouseEventsUtils.ts +32 -0
@@ -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',
@@ -90,7 +96,7 @@ export class ChangeManager {
90
96
  )
91
97
  if (!results2.ok) {
92
98
  // notify of invalid change and revert
93
- await this.revert(change)
99
+ await this.undo(change)
94
100
  }
95
101
 
96
102
  if (submitToBackend) {
@@ -100,7 +106,9 @@ export class ChangeManager {
100
106
  // submit to driver
101
107
  const { collaborationServerDriver, getBackendDriver } = this.dataStore
102
108
  const backendDriver = isAssemblySpecificChange(change)
103
- ? 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
104
112
  : collaborationServerDriver
105
113
  let backendResult: ValidationResultSet
106
114
  try {
@@ -111,7 +119,7 @@ export class ChangeManager {
111
119
  }
112
120
  console.error(error)
113
121
  session.notify(String(error), 'error')
114
- await this.revert(change, false)
122
+ await this.undo(change, false)
115
123
  return
116
124
  }
117
125
  if (!backendResult.ok) {
@@ -120,15 +128,15 @@ export class ChangeManager {
120
128
  jobsManager.abortJob(job.name, msg)
121
129
  }
122
130
  session.notify(msg, 'error')
123
- await this.revert(change, false)
131
+ await this.undo(change, false)
124
132
  return
125
133
  }
126
134
  if (change.notification) {
127
135
  session.notify(change.notification, 'success')
128
136
  }
129
137
  if (addToRecents) {
130
- // Push the change into array
131
138
  this.recentChanges.push(change)
139
+ this.undoneChanges = []
132
140
  }
133
141
  }
134
142
 
@@ -137,22 +145,36 @@ export class ChangeManager {
137
145
  }
138
146
  }
139
147
 
140
- async revert(change: Change, submitToBackend = true) {
148
+ async undo(change: Change, submitToBackend = true) {
141
149
  const inverseChange = change.getInverse()
142
150
  const opts = { submitToBackend, addToRecents: false }
143
151
  return this.submit(inverseChange, opts)
144
152
  }
145
153
 
146
- /**
147
- * Undo the last change
148
- */
149
- async revertLastChange() {
154
+ async redo(change: Change, submitToBackend = true) {
155
+ const opts = { submitToBackend, addToRecents: false }
156
+ return this.submit(change, opts)
157
+ }
158
+
159
+ async undoLastChange() {
160
+ const session = getSession(this.dataStore)
150
161
  const lastChange = this.recentChanges.pop()
151
162
  if (!lastChange) {
152
- const session = getSession(this.dataStore)
153
- session.notify('No changes to undo!', 'warning')
163
+ session.notify('No changes to undo!', 'info')
164
+ return
165
+ }
166
+ this.undoneChanges.push(lastChange)
167
+ return this.undo(lastChange)
168
+ }
169
+
170
+ async redoLastChange() {
171
+ const session = getSession(this.dataStore)
172
+ const lastChange = this.undoneChanges.pop()
173
+ if (!lastChange) {
174
+ session.notify('No changes to redo!', 'info')
154
175
  return
155
176
  }
156
- return this.revert(lastChange)
177
+ this.recentChanges.push(lastChange)
178
+ return this.redo(lastChange)
157
179
  }
158
180
  }
@@ -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
  },
@@ -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
  }}
@@ -28,10 +28,18 @@ type SegmentListType = 'CDS' | 'cDNA' | 'genomic' | 'protein'
28
28
 
29
29
  interface SequenceSegment {
30
30
  type: SegmentType
31
- sequenceLines: string[]
31
+ sequence: string
32
32
  locs: { min: number; max: number }[]
33
33
  }
34
34
 
35
+ function getSequenceLength(segments: SequenceSegment[]): number {
36
+ let length = 0
37
+ for (const segment of segments) {
38
+ length += segment.sequence.length
39
+ }
40
+ return length
41
+ }
42
+
35
43
  function getSequenceSegments(
36
44
  segmentType: SegmentListType,
37
45
  feature: AnnotationFeature,
@@ -57,51 +65,20 @@ function getSequenceSegments(
57
65
  : loc.type
58
66
  const previousSegment = segments.at(-1)
59
67
  if (!previousSegment) {
60
- const sequenceLines = splitStringIntoChunks(
61
- sequence,
62
- SEQUENCE_WRAP_LENGTH,
63
- )
64
68
  segments.push({
65
69
  type,
66
- sequenceLines,
70
+ sequence,
67
71
  locs: [{ min: loc.min, max: loc.max }],
68
72
  })
69
73
  continue
70
74
  }
71
75
  if (previousSegment.type === type) {
72
- const [previousSegmentFirstLine, ...previousSegmentFollowingLines] =
73
- previousSegment.sequenceLines
74
- const newSequence = previousSegmentFollowingLines.join('') + sequence
75
- previousSegment.sequenceLines = [
76
- previousSegmentFirstLine,
77
- ...splitStringIntoChunks(newSequence, SEQUENCE_WRAP_LENGTH),
78
- ]
76
+ previousSegment.sequence += sequence
79
77
  previousSegment.locs.push({ min: loc.min, max: loc.max })
80
78
  } else {
81
- const count = segments.reduce(
82
- (accumulator, currentSegment) =>
83
- accumulator +
84
- currentSegment.sequenceLines.reduce(
85
- (subAccumulator, currentLine) =>
86
- subAccumulator + currentLine.length,
87
- 0,
88
- ),
89
- 0,
90
- )
91
- const previousLineLength = count % SEQUENCE_WRAP_LENGTH
92
- const newSegmentFirstLineLength =
93
- SEQUENCE_WRAP_LENGTH - previousLineLength
94
- const newSegmentFirstLine = sequence.slice(
95
- 0,
96
- newSegmentFirstLineLength,
97
- )
98
- const newSegmentRemainderLines = splitStringIntoChunks(
99
- sequence.slice(newSegmentFirstLineLength),
100
- SEQUENCE_WRAP_LENGTH,
101
- )
102
79
  segments.push({
103
80
  type,
104
- sequenceLines: [newSegmentFirstLine, ...newSegmentRemainderLines],
81
+ sequence,
105
82
  locs: [{ min: loc.min, max: loc.max }],
106
83
  })
107
84
  }
@@ -113,17 +90,14 @@ function getSequenceSegments(
113
90
  const [firstLocation] = cdsLocations
114
91
  const locs: { min: number; max: number }[] = []
115
92
  for (const loc of firstLocation) {
116
- wholeSequence += getSequence(loc.min, loc.max)
93
+ let locSeq = getSequence(loc.min, loc.max)
94
+ if (strand === -1) {
95
+ locSeq = revcom(locSeq)
96
+ }
97
+ wholeSequence += locSeq
117
98
  locs.push({ min: loc.min, max: loc.max })
118
99
  }
119
- if (strand === -1) {
120
- wholeSequence = revcom(wholeSequence)
121
- }
122
- const sequenceLines = splitStringIntoChunks(
123
- wholeSequence,
124
- SEQUENCE_WRAP_LENGTH,
125
- )
126
- segments.push({ type: 'CDS', sequenceLines, locs })
100
+ segments.push({ type: 'CDS', sequence: wholeSequence, locs })
127
101
  return segments
128
102
  }
129
103
  case 'protein': {
@@ -131,20 +105,20 @@ function getSequenceSegments(
131
105
  const [firstLocation] = cdsLocations
132
106
  const locs: { min: number; max: number }[] = []
133
107
  for (const loc of firstLocation) {
134
- wholeSequence += getSequence(loc.min, loc.max)
108
+ let locSeq = getSequence(loc.min, loc.max)
109
+ if (strand === -1) {
110
+ locSeq = revcom(locSeq)
111
+ }
112
+ wholeSequence += locSeq
135
113
  locs.push({ min: loc.min, max: loc.max })
136
114
  }
137
- if (strand === -1) {
138
- wholeSequence = revcom(wholeSequence)
139
- }
140
115
  let protein = ''
141
116
  for (let i = 0; i < wholeSequence.length; i += 3) {
142
117
  const codonSeq: string = wholeSequence.slice(i, i + 3).toUpperCase()
143
118
  protein +=
144
119
  defaultCodonTable[codonSeq as keyof typeof defaultCodonTable] || '&'
145
120
  }
146
- const sequenceLines = splitStringIntoChunks(protein, SEQUENCE_WRAP_LENGTH)
147
- segments.push({ type: 'protein', sequenceLines, locs })
121
+ segments.push({ type: 'protein', sequence: protein, locs })
148
122
  return segments
149
123
  }
150
124
  }
@@ -282,6 +256,43 @@ export const TranscriptSequence = observer(function TranscriptSequence({
282
256
  void copyToClipboard(seqDiv)
283
257
  }
284
258
 
259
+ function wrapSequence(
260
+ sequenceSegments: SequenceSegment[],
261
+ sequenceWrapLength: number,
262
+ ): React.ReactNode[] {
263
+ const seqElements: React.ReactNode[] = []
264
+ let processedChars = 0
265
+ for (const [index, segment] of sequenceSegments.entries()) {
266
+ const lastLineLength = processedChars % sequenceWrapLength
267
+ const segmentLineBreak =
268
+ processedChars > 0 && lastLineLength === 0 ? '\n' : ''
269
+ processedChars += segment.sequence.length
270
+ const firstLine =
271
+ segmentLineBreak +
272
+ segment.sequence.slice(0, sequenceWrapLength - lastLineLength)
273
+ const remainingLines = splitStringIntoChunks(
274
+ segment.sequence.slice(firstLine.length),
275
+ sequenceWrapLength,
276
+ )
277
+ const printLines = [firstLine, ...remainingLines]
278
+
279
+ const span = (
280
+ <span
281
+ key={`${segment.type}-${index}`}
282
+ style={{
283
+ background: getSegmentColor(segment.type),
284
+ color: theme.palette.getContrastText(getSegmentColor(segment.type)),
285
+ whiteSpace: 'pre-line',
286
+ }}
287
+ >
288
+ {printLines.join('\n')}
289
+ </span>
290
+ )
291
+ seqElements.push(span)
292
+ }
293
+ return seqElements
294
+ }
295
+
285
296
  return (
286
297
  <>
287
298
  <Select
@@ -289,9 +300,14 @@ export const TranscriptSequence = observer(function TranscriptSequence({
289
300
  value={selectedOption}
290
301
  onChange={handleChangeSeqOption}
291
302
  size="small"
303
+ data-testid="sequenceOptionSelector"
292
304
  >
293
305
  {sequenceOptions.map((option) => (
294
- <MenuItem key={option} value={option}>
306
+ <MenuItem
307
+ key={option}
308
+ value={option}
309
+ data-testid={`sequenceOption-${option}`}
310
+ >
295
311
  {option}
296
312
  </MenuItem>
297
313
  ))}
@@ -320,29 +336,10 @@ export const TranscriptSequence = observer(function TranscriptSequence({
320
336
  : `${interval.max}-${interval.min + 1}`,
321
337
  )
322
338
  .join(';')}
323
- ({feature.strand === 1 ? '+' : '-'})
339
+ (strand={feature.strand === 1 ? '+' : '-'};length=
340
+ {getSequenceLength(sequenceSegments)})
324
341
  <br />
325
- {sequenceSegments.map((segment, index) => (
326
- <span
327
- key={`${segment.type}-${index}`}
328
- style={{
329
- background: getSegmentColor(segment.type),
330
- color: theme.palette.getContrastText(
331
- getSegmentColor(segment.type),
332
- ),
333
- }}
334
- >
335
- {segment.sequenceLines.map((sequenceLine, idx) => (
336
- <React.Fragment key={`${sequenceLine.slice(0, 5)}-${idx}`}>
337
- {sequenceLine}
338
- {idx === segment.sequenceLines.length - 1 &&
339
- sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (
340
- <br />
341
- )}
342
- </React.Fragment>
343
- ))}
344
- </span>
345
- ))}
342
+ {wrapSequence(sequenceSegments, SEQUENCE_WRAP_LENGTH)}
346
343
  </Paper>
347
344
  </>
348
345
  )