@teselagen/ove 0.8.29 → 0.8.30

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teselagen/ove",
3
- "version": "0.8.29",
3
+ "version": "0.8.30",
4
4
  "main": "./src/index.js",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/TeselaGen/tg-oss",
@@ -19,8 +19,8 @@
19
19
  "@teselagen/file-utils": "0.3.23",
20
20
  "@teselagen/range-utils": "0.3.20",
21
21
  "@teselagen/react-list": "0.8.18",
22
- "@teselagen/sequence-utils": "0.3.41",
23
- "@teselagen/ui": "0.10.17",
22
+ "@teselagen/sequence-utils": "0.3.42",
23
+ "@teselagen/ui": "0.10.18",
24
24
  "@use-gesture/react": "10.3.0",
25
25
  "biomsa": "^0.2.4",
26
26
  "classnames": "^2.3.2",
@@ -1,12 +1,4 @@
1
- declare const _default: ((state: any) => {
2
- cutsitesByName: {};
3
- cutsitesById: {};
4
- cutsitesArray: never[];
5
- }) & import('reselect').OutputSelectorFields<(...args: any) => {
6
- cutsitesByName: {};
7
- cutsitesById: {};
8
- cutsitesArray: never[];
9
- }, {
1
+ declare const _default: ((state: any, ...params: any[]) => any) & import('reselect').OutputSelectorFields<(...args: readonly unknown[]) => any, {
10
2
  clearCache: () => void;
11
3
  }> & {
12
4
  clearCache: () => void;
@@ -1,4 +1,8 @@
1
- declare const _default: ((state: any, ...params: any[]) => any) & import('reselect').OutputSelectorFields<(...args: readonly unknown[]) => any, {
1
+ declare const _default: ((state: any) => {
2
+ cutsitesByName: {};
3
+ }) & import('reselect').OutputSelectorFields<(args_0: any, args_1: any, args_2: any, args_3: any) => {
4
+ cutsitesByName: {};
5
+ }, {
2
6
  clearCache: () => void;
3
7
  }> & {
4
8
  clearCache: () => void;
@@ -361,6 +361,7 @@ export class Editor extends React.Component {
361
361
  hoveredId,
362
362
  isFullscreen,
363
363
  maxInsertSize,
364
+ getAcceptedInsertChars,
364
365
  showAminoAcidUnitAsCodon,
365
366
  maxAnnotationsToDisplay,
366
367
  minHeight = 400,
@@ -595,6 +596,7 @@ export class Editor extends React.Component {
595
596
  {...panelPropsToSpread}
596
597
  editorName={editorName}
597
598
  maxInsertSize={maxInsertSize}
599
+ getAcceptedInsertChars={getAcceptedInsertChars}
598
600
  showAminoAcidUnitAsCodon={showAminoAcidUnitAsCodon}
599
601
  isProtein={sequenceData.isProtein}
600
602
  onlyShowLabelsThatDoNotFit={onlyShowLabelsThatDoNotFit}
@@ -51,6 +51,8 @@ export const userDefinedHandlersAndOpts = [
51
51
  "enzymeManageOverride",
52
52
  "enzymeGroupsOverride",
53
53
  "additionalEnzymes",
54
+ "getAcceptedInsertChars",
55
+ "maxInsertSize",
54
56
  "onDelete",
55
57
  "onCopy",
56
58
  "autoAnnotateFeatures",
@@ -41,12 +41,23 @@ const Dialogs = {
41
41
  export function GlobalDialog(props) {
42
42
  const { dialogOverrides = {}, editorName } = props;
43
43
  const [uniqKey, setUniqKeyToForceRerender] = useState();
44
+
44
45
  useEffect(() => {
45
46
  //on unmount, clear the global dialog state..
46
47
  return () => {
47
48
  hideDialog();
48
49
  };
49
50
  }, []);
51
+
52
+ useEffect(() => {
53
+ dialogHolder.setUniqKeyToForceRerender = setUniqKeyToForceRerender;
54
+
55
+ if (editorName) {
56
+ const slot = (dialogHolder[editorName] = dialogHolder[editorName] || {});
57
+ slot.setUniqKeyToForceRerender = setUniqKeyToForceRerender;
58
+ }
59
+ }, [editorName]);
60
+
50
61
  if (
51
62
  dialogHolder.editorName &&
52
63
  editorName &&
@@ -54,7 +65,6 @@ export function GlobalDialog(props) {
54
65
  ) {
55
66
  return null;
56
67
  }
57
- dialogHolder.setUniqKeyToForceRerender = setUniqKeyToForceRerender;
58
68
  const Comp =
59
69
  dialogHolder.CustomModalComponent ||
60
70
  dialogOverrides[dialogHolder.overrideName] ||
@@ -15,6 +15,8 @@ export function showDialog({
15
15
  if (!dialogHolder.dialogType && ModalComponent) {
16
16
  dialogHolder.dialogType = "TGCustomModal";
17
17
  }
18
+
19
+ dialogHolder.editorName = props?.editorName;
18
20
  // check if focused element in the dom is within a given editor and add an editor prop to the dialog
19
21
  if (document.activeElement && document.activeElement.closest(".veEditor")) {
20
22
  let editorName;
@@ -36,15 +38,25 @@ export function showDialog({
36
38
  dialogHolder.CustomModalComponent = ModalComponent;
37
39
  dialogHolder.props = props;
38
40
  dialogHolder.overrideName = overrideName;
39
- dialogHolder.setUniqKeyToForceRerender(shortid());
41
+ if (dialogHolder.editorName && dialogHolder?.[dialogHolder.editorName]) {
42
+ dialogHolder?.[dialogHolder.editorName]?.setUniqKeyToForceRerender?.(
43
+ shortid()
44
+ );
45
+ } else {
46
+ dialogHolder?.setUniqKeyToForceRerender?.(shortid());
47
+ }
40
48
  }
41
49
  export function hideDialog() {
42
50
  delete dialogHolder.dialogType;
43
51
  delete dialogHolder.CustomModalComponent;
44
52
  delete dialogHolder.props;
45
53
  delete dialogHolder.overrideName;
54
+ if (dialogHolder.editorName && dialogHolder?.[dialogHolder.editorName]) {
55
+ dialogHolder?.[dialogHolder.editorName]?.setUniqKeyToForceRerender?.();
56
+ } else {
57
+ dialogHolder?.setUniqKeyToForceRerender?.();
58
+ }
46
59
  delete dialogHolder.editorName;
47
- dialogHolder.setUniqKeyToForceRerender();
48
60
  }
49
61
 
50
62
  const typeToDialogType = {
@@ -4,14 +4,101 @@ import withHover from "../../helperComponents/withHover";
4
4
  import getAnnotationNameAndStartStopString from "../../utils/getAnnotationNameAndStartStopString";
5
5
 
6
6
  import React, { useState } from "react";
7
- import { doesLabelFitInAnnotation } from "../utils";
7
+ import { doesLabelFitInAnnotation, getAnnotationTextWidth } from "../utils";
8
8
  import { noop } from "lodash-es";
9
9
  import getAnnotationClassnames from "../../utils/getAnnotationClassnames";
10
- import { getStripedPattern } from "../../utils/editorUtils";
11
10
  import { ANNOTATION_LABEL_FONT_WIDTH } from "../constants";
11
+ import { getStripedPattern } from "../../utils/editorUtils";
12
12
  import { partOverhangs } from "../partOverhangs";
13
13
  import { Tooltip } from "@blueprintjs/core";
14
14
 
15
+ function getAnnotationTextOffset({
16
+ width,
17
+ nameToDisplay,
18
+ hasAPoint,
19
+ pointiness,
20
+ forward
21
+ }) {
22
+ return (
23
+ width / 2 -
24
+ getAnnotationTextWidth(nameToDisplay) / 2 -
25
+ (hasAPoint
26
+ ? (pointiness / 2 + ANNOTATION_LABEL_FONT_WIDTH / 2) * (forward ? 1 : -1)
27
+ : 0)
28
+ );
29
+ }
30
+
31
+ function getAnnotationNameInfo({
32
+ name,
33
+ width,
34
+ hasAPoint,
35
+ pointiness,
36
+ forward,
37
+ charWidth,
38
+ truncateLabelsThatDoNotFit,
39
+ onlyShowLabelsThatDoNotFit,
40
+ annotation
41
+ }) {
42
+ let nameToDisplay = name;
43
+ let textOffset = getAnnotationTextOffset({
44
+ width,
45
+ nameToDisplay,
46
+ hasAPoint,
47
+ pointiness,
48
+ forward
49
+ });
50
+ const widthAvailableForText = width - ANNOTATION_LABEL_FONT_WIDTH * 2;
51
+ if (
52
+ !doesLabelFitInAnnotation(name, { width }, charWidth) ||
53
+ (!onlyShowLabelsThatDoNotFit &&
54
+ ["parts", "features"].includes(annotation.annotationTypePlural))
55
+ ) {
56
+ if (truncateLabelsThatDoNotFit) {
57
+ // Binary search for max fitting substring
58
+ let left = 0;
59
+ let right = name.length;
60
+ let bestFit = "";
61
+
62
+ while (left <= right) {
63
+ const mid = Math.floor((left + right) / 2);
64
+ const candidate = name.slice(0, mid);
65
+ const candidateWidth = getAnnotationTextWidth(candidate);
66
+
67
+ if (candidateWidth <= widthAvailableForText) {
68
+ if (candidate.length > bestFit.length) {
69
+ bestFit = candidate;
70
+ }
71
+ left = mid + 1;
72
+ } else {
73
+ right = mid - 1;
74
+ }
75
+ }
76
+ if (bestFit.length < name.length) {
77
+ bestFit = bestFit.slice(0, -2) + "..";
78
+ }
79
+
80
+ nameToDisplay = bestFit;
81
+
82
+ if (nameToDisplay.length <= 3) {
83
+ textOffset = 0;
84
+ nameToDisplay = "";
85
+ } else {
86
+ textOffset = getAnnotationTextOffset({
87
+ width,
88
+ nameToDisplay,
89
+ hasAPoint,
90
+ pointiness,
91
+ forward
92
+ });
93
+ }
94
+ } else {
95
+ textOffset = 0;
96
+ nameToDisplay = "";
97
+ }
98
+ }
99
+ return { textOffset, nameToDisplay };
100
+ }
101
+
15
102
  function PointedAnnotation(props) {
16
103
  const {
17
104
  className,
@@ -165,39 +252,19 @@ function PointedAnnotation(props) {
165
252
  Q ${pointiness},${height / 2} ${0},${0}
166
253
  z`;
167
254
  }
168
- let nameToDisplay = name;
169
- let textOffset =
170
- width / 2 -
171
- (name.length * 5) / 2 -
172
- (hasAPoint ? (pointiness / 2) * (forward ? 1 : -1) : 0);
173
- if (
174
- !doesLabelFitInAnnotation(name, { width }, charWidth) ||
175
- (!onlyShowLabelsThatDoNotFit &&
176
- ["parts", "features"].includes(annotation.annotationTypePlural))
177
- ) {
178
- if (truncateLabelsThatDoNotFit) {
179
- const fractionToDisplay =
180
- width / (name.length * ANNOTATION_LABEL_FONT_WIDTH);
181
- const numLetters = Math.floor(fractionToDisplay * name.length);
182
- nameToDisplay = name.slice(0, numLetters);
183
- if (nameToDisplay.length > 3) {
184
- if (nameToDisplay.length !== name.length) {
185
- nameToDisplay += "..";
186
- }
187
255
 
188
- textOffset =
189
- width / 2 -
190
- (nameToDisplay.length * 5) / 2 -
191
- (hasAPoint ? (pointiness / 2) * (forward ? 1 : -1) : 0);
192
- } else {
193
- textOffset = 0;
194
- nameToDisplay = "";
195
- }
196
- } else {
197
- textOffset = 0;
198
- nameToDisplay = "";
199
- }
200
- }
256
+ const { textOffset, nameToDisplay } = getAnnotationNameInfo({
257
+ name,
258
+ width,
259
+ hasAPoint,
260
+ pointiness,
261
+ forward,
262
+ charWidth,
263
+ truncateLabelsThatDoNotFit,
264
+ onlyShowLabelsThatDoNotFit,
265
+ annotation
266
+ });
267
+
201
268
  let _textColor = textColor;
202
269
  if (!textColor) {
203
270
  try {
@@ -1,14 +1,30 @@
1
1
  import { ANNOTATION_LABEL_FONT_WIDTH } from "./constants";
2
2
  import { getWidth } from "./getXStartAndWidthOfRowAnnotation";
3
3
 
4
+ // Cache canvas context for text measurement
5
+ let measureCanvas;
6
+ export function getAnnotationTextWidth(
7
+ text,
8
+ fontSize = ANNOTATION_LABEL_FONT_WIDTH,
9
+ fontFamily = "monospace"
10
+ ) {
11
+ if (!measureCanvas) {
12
+ measureCanvas = document.createElement("canvas");
13
+ }
14
+ const ctx = measureCanvas.getContext("2d");
15
+ ctx.font = `${fontSize}px ${fontFamily}`;
16
+ return ctx.measureText(text).width;
17
+ }
18
+
4
19
  export const doesLabelFitInAnnotation = (
5
20
  text = "",
6
21
  { range, width },
7
22
  charWidth
8
23
  ) => {
9
- const textLength = text.length * ANNOTATION_LABEL_FONT_WIDTH;
10
- const widthMinusOne =
11
- (range ? getWidth(range, charWidth, 0) : width) - charWidth;
24
+ const textLength = getAnnotationTextWidth(text);
25
+ const widthMinusOne = range
26
+ ? getWidth(range, charWidth, 0) - ANNOTATION_LABEL_FONT_WIDTH * 2
27
+ : width - ANNOTATION_LABEL_FONT_WIDTH * 2;
12
28
  return widthMinusOne > textLength;
13
29
  };
14
30
 
@@ -284,7 +284,7 @@ const fileCommandDefs = {
284
284
  },
285
285
  exportSequenceAsTeselagenJson: {
286
286
  name: "Download Teselagen JSON File",
287
- handler: props => props.exportSequenceToFile("teselagenJson")
287
+ handler: props => props.exportSequenceToFile("teselagenJson", { getAcceptedInsertChars: props.getAcceptedInsertChars})
288
288
  },
289
289
 
290
290
  viewProperties: {
@@ -10,10 +10,9 @@ import { getRangeLength } from "@teselagen/range-utils";
10
10
  import { getOrfColor } from "../../constants/orfFrameToColorMap";
11
11
  import { connectToEditor } from "../../withEditorProps";
12
12
  import { compose } from "recompose";
13
- import selectors from "../../selectors";
14
13
 
15
14
  import getCommands from "../../commands";
16
- import { sizeSchema } from "./utils";
15
+ import { sizeSchema, getMemoOrfs } from "./utils";
17
16
  import { orfsSubmenu } from "../../MenuBar/viewSubmenu";
18
17
  import { getVisFilter } from "./GenericAnnotationProperties";
19
18
 
@@ -107,7 +106,7 @@ export default compose(
107
106
  readOnly,
108
107
  annotationVisibility,
109
108
  useAdditionalOrfStartCodons,
110
- orfs: selectors.orfsSelector(editorState),
109
+ orfs: getMemoOrfs(editorState),
111
110
  sequenceLength: sequence.length,
112
111
  sequenceData,
113
112
  minimumOrfSize
@@ -14,6 +14,7 @@ import selectors from "../../selectors";
14
14
  import { getMassOfAaString } from "@teselagen/sequence-utils";
15
15
  import { translationsSubmenu } from "../../MenuBar/viewSubmenu";
16
16
  import { getVisFilter } from "./GenericAnnotationProperties";
17
+ import { getMemoOrfs } from "./utils";
17
18
 
18
19
  class TranslationProperties extends React.Component {
19
20
  constructor(props) {
@@ -140,7 +141,7 @@ export default compose(
140
141
  return {
141
142
  readOnly,
142
143
  translations: selectors.translationsSelector(editorState),
143
- orfs: selectors.orfsSelector(editorState),
144
+ orfs: getMemoOrfs(editorState),
144
145
  annotationVisibility,
145
146
  sequenceLength: (sequenceData.sequence || "").length,
146
147
  sequenceData
@@ -1,6 +1,8 @@
1
1
  import React from "react";
2
+ import { isEqual } from "lodash-es";
2
3
  import { convertDnaCaretPositionOrRangeToAA } from "@teselagen/sequence-utils";
3
4
  import { convertRangeTo1Based } from "@teselagen/range-utils";
5
+ import selectors from "../../selectors";
4
6
 
5
7
  export const sizeSchema = isProtein => ({
6
8
  path: "size",
@@ -35,3 +37,30 @@ export const sizeSchema = isProtein => ({
35
37
  );
36
38
  }
37
39
  });
40
+
41
+ export const getMemoOrfs = (() => {
42
+ let lastDeps;
43
+ let lastResult;
44
+ return (editorState) => {
45
+ const {
46
+ sequenceData,
47
+ minimumOrfSize,
48
+ useAdditionalOrfStartCodons
49
+ } = editorState;
50
+
51
+ const { sequence, circular } = sequenceData;
52
+
53
+ const deps = {
54
+ sequence,
55
+ circular,
56
+ minimumOrfSize,
57
+ useAdditionalOrfStartCodons
58
+ };
59
+ if (lastResult && isEqual(deps, lastDeps)) {
60
+ return lastResult;
61
+ }
62
+ lastResult = selectors.orfsSelector(editorState);
63
+ lastDeps = deps;
64
+ return lastResult;
65
+ };
66
+ })();
@@ -4,12 +4,55 @@ import sequenceSelector from "./sequenceSelector";
4
4
  import restrictionEnzymesSelector from "./restrictionEnzymesSelector";
5
5
  import cutsiteLabelColorSelector from "./cutsiteLabelColorSelector";
6
6
  import { createSelector } from "reselect";
7
+ import { isEqual } from "lodash-es";
7
8
 
8
9
  import { flatMap as flatmap, map } from "lodash-es";
9
10
  import { getCutsitesFromSequence } from "@teselagen/sequence-utils";
10
11
  import { getLowerCaseObj } from "../utils/arrayUtils";
11
12
 
12
- function cutsitesSelector(sequence, circular, enzymeList, cutsiteLabelColors) {
13
+ // [{ args: {sequence,circular,enzymeList,cutsiteLabelColors}, result }]
14
+ const cutsitesCache = [];
15
+
16
+ function getCachedResult(argsObj) {
17
+ const idx = cutsitesCache.findIndex(
18
+ entry =>
19
+ entry &&
20
+ isEqual(entry.args, argsObj)
21
+ );
22
+ if (idx === -1) return;
23
+ const hit = cutsitesCache[idx];
24
+ return hit.result;
25
+ }
26
+
27
+ function setCachedResult(
28
+ argsObj,
29
+ result,
30
+ cacheSize = 1
31
+ ) {
32
+ cutsitesCache.push({
33
+ args: argsObj,
34
+ result
35
+ });
36
+ //keep cache size manageable
37
+ if (cutsitesCache.length > cacheSize) cutsitesCache.shift();
38
+ }
39
+
40
+ function cutsitesSelector(
41
+ sequence,
42
+ circular,
43
+ enzymeList,
44
+ cutsiteLabelColors,
45
+ editorSize = 1
46
+ ) {
47
+ const cachedResult = getCachedResult({
48
+ sequence,
49
+ circular,
50
+ enzymeList,
51
+ cutsiteLabelColors
52
+ });
53
+ if (cachedResult) {
54
+ return cachedResult;
55
+ }
13
56
  //get the cutsites grouped by enzyme
14
57
  const cutsitesByName = getLowerCaseObj(
15
58
  getCutsitesFromSequence(sequence, circular, map(enzymeList))
@@ -45,11 +88,22 @@ function cutsitesSelector(sequence, circular, enzymeList, cutsiteLabelColors) {
45
88
  const cutsitesArray = flatmap(cutsitesByName, function (cutsitesForEnzyme) {
46
89
  return cutsitesForEnzyme;
47
90
  });
48
- return {
91
+ const result = {
49
92
  cutsitesByName,
50
93
  cutsitesById,
51
94
  cutsitesArray
52
95
  };
96
+ setCachedResult(
97
+ {
98
+ sequence,
99
+ circular,
100
+ enzymeList,
101
+ cutsiteLabelColors
102
+ },
103
+ result,
104
+ editorSize
105
+ );
106
+ return result;
53
107
  }
54
108
 
55
109
  export default createSelector(
@@ -57,5 +111,6 @@ export default createSelector(
57
111
  circularSelector,
58
112
  restrictionEnzymesSelector,
59
113
  cutsiteLabelColorSelector,
114
+ editorState => editorState.editorSize,
60
115
  cutsitesSelector
61
116
  );
@@ -82,6 +82,7 @@ class SequenceInputNoHotkeys extends React.Component {
82
82
  caretPosition,
83
83
  sequenceData,
84
84
  maxInsertSize,
85
+ getAcceptedInsertChars,
85
86
  showAminoAcidUnitAsCodon
86
87
  } = this.props;
87
88
  const { charsToInsert, hasTempError } = this.state;
@@ -151,8 +152,9 @@ class SequenceInputNoHotkeys extends React.Component {
151
152
  e.target.value,
152
153
  {
153
154
  ...sequenceData,
154
- name: undefined
155
- }
155
+ name: undefined,
156
+ getAcceptedInsertChars
157
+ },
156
158
  );
157
159
  if (warnings.length) {
158
160
  this.setState({
@@ -212,7 +212,8 @@ function VectorInteractionHOC(Component /* options */) {
212
212
  onPaste,
213
213
  disableBpEditing,
214
214
  sequenceData,
215
- maxInsertSize
215
+ maxInsertSize,
216
+ getAcceptedInsertChars
216
217
  } = this.props;
217
218
 
218
219
  if (disableBpEditing) {
@@ -260,7 +261,8 @@ function VectorInteractionHOC(Component /* options */) {
260
261
  topLevelSeqData: sequenceData,
261
262
  provideNewIdsForAnnotations: true,
262
263
  annotationsAsObjects: true,
263
- noCdsTranslations: true
264
+ noCdsTranslations: true,
265
+ getAcceptedInsertChars
264
266
  });
265
267
  if (!seqDataToInsert.sequence.length)
266
268
  return window.toastr.warning("Sorry no valid base pairs to paste");
@@ -283,7 +285,8 @@ function VectorInteractionHOC(Component /* options */) {
283
285
  selectionLayer,
284
286
  copyOptions,
285
287
  disableBpEditing,
286
- readOnly
288
+ readOnly,
289
+ getAcceptedInsertChars,
287
290
  } = this.props;
288
291
  const onCut = this.props.onCut || this.props.onCopy || noop;
289
292
  const seqData = tidyUpSequenceData(
@@ -308,7 +311,8 @@ function VectorInteractionHOC(Component /* options */) {
308
311
  {
309
312
  doNotRemoveInvalidChars: true,
310
313
  annotationsAsObjects: true,
311
- includeProteinSequence: true
314
+ includeProteinSequence: true,
315
+ getAcceptedInsertChars
312
316
  }
313
317
  );
314
318
 
@@ -340,7 +344,8 @@ function VectorInteractionHOC(Component /* options */) {
340
344
  e,
341
345
  tidyUpSequenceData(seqData, {
342
346
  doNotRemoveInvalidChars: true,
343
- annotationsAsObjects: true
347
+ annotationsAsObjects: true,
348
+ getAcceptedInsertChars
344
349
  }),
345
350
  this.props
346
351
  );
@@ -404,6 +409,7 @@ function VectorInteractionHOC(Component /* options */) {
404
409
  readOnly,
405
410
  disableBpEditing,
406
411
  maxInsertSize,
412
+ getAcceptedInsertChars,
407
413
  showAminoAcidUnitAsCodon
408
414
  } = this.props;
409
415
  const sequenceLength = sequenceData.sequence.length;
@@ -423,6 +429,7 @@ function VectorInteractionHOC(Component /* options */) {
423
429
  sequenceLength,
424
430
  caretPosition,
425
431
  maxInsertSize,
432
+ getAcceptedInsertChars,
426
433
  showAminoAcidUnitAsCodon,
427
434
  handleInsert: async seqDataToInsert => {
428
435
  await insertAndSelectHelper({
@@ -39,6 +39,7 @@ import { createSelector, defaultMemoize } from "reselect";
39
39
  import domtoimage from "dom-to-image";
40
40
  import {
41
41
  hideDialog,
42
+ dialogHolder,
42
43
  showAddOrEditAnnotationDialog,
43
44
  showDialog
44
45
  } from "../GlobalDialogUtils";
@@ -141,7 +142,8 @@ export const handleSave =
141
142
  readOnly,
142
143
  alwaysAllowSave,
143
144
  sequenceData,
144
- lastSavedIdUpdate
145
+ lastSavedIdUpdate,
146
+ getAcceptedInsertChars
145
147
  } = props;
146
148
  const saveHandler = opts.isSaveAs ? onSaveAs || onSave : onSave;
147
149
 
@@ -162,7 +164,8 @@ export const handleSave =
162
164
  opts,
163
165
  tidyUpSequenceData(sequenceData, {
164
166
  doNotRemoveInvalidChars: true,
165
- annotationsAsObjects: true
167
+ annotationsAsObjects: true,
168
+ getAcceptedInsertChars
166
169
  }),
167
170
  props,
168
171
  updateLastSavedIdToCurrent
@@ -367,8 +370,8 @@ export default compose(
367
370
  options
368
371
  } = props.beforeSequenceInsertOrDelete
369
372
  ? (await props.beforeSequenceInsertOrDelete(
370
- tidyUpSequenceData(_sequenceDataToInsert),
371
- tidyUpSequenceData(_existingSequenceData),
373
+ tidyUpSequenceData(_sequenceDataToInsert, { getAcceptedInsertChars: props.getAcceptedInsertChars }),
374
+ tidyUpSequenceData(_existingSequenceData, { getAcceptedInsertChars: props.getAcceptedInsertChars }),
372
375
  _caretPositionOrRange,
373
376
  _options
374
377
  )) || {}
@@ -555,7 +558,11 @@ const getEditorState = createSelector(
555
558
  state => state.VectorEditor,
556
559
  (state, editorName) => editorName,
557
560
  (VectorEditor, editorName) => {
558
- return VectorEditor[editorName];
561
+ const editorState = VectorEditor[editorName];
562
+ editorState && (editorState.editorSize = Object.values(VectorEditor).filter(
563
+ editorItem => editorItem?.sequenceData
564
+ ).length);
565
+ return editorState;
559
566
  }
560
567
  );
561
568
 
@@ -595,6 +602,10 @@ function mapStateToProps(state, ownProps) {
595
602
  annotationTypePlural,
596
603
  sequenceLength
597
604
  );
605
+ if (dialogHolder.editorName) {
606
+ annotationToAdd =
607
+ dialogHolder.editorName === editorName ? annotationToAdd : undefined;
608
+ }
598
609
  }
599
610
  });
600
611
 
@@ -623,7 +634,7 @@ function mapStateToProps(state, ownProps) {
623
634
  const selectedCutsites = s.selectedCutsitesSelector(editorState);
624
635
  const allCutsites = s.cutsitesSelector(
625
636
  editorState,
626
- ownProps.additionalEnzymes
637
+ ownProps.additionalEnzymes,
627
638
  );
628
639
 
629
640
  const { matchedSearchLayer, searchLayers, matchesTotal } =
@@ -887,13 +898,15 @@ export function getShowGCContent(state, ownProps) {
887
898
  return toRet;
888
899
  }
889
900
 
890
- function jsonToJson(incomingJson) {
901
+ function jsonToJson(incomingJson, options) {
902
+ const {getAcceptedInsertChars} = options || {};
891
903
  return JSON.stringify(
892
904
  omit(
893
905
  cleanUpTeselagenJsonForExport(
894
906
  tidyUpSequenceData(incomingJson, {
895
907
  doNotRemoveInvalidChars: true,
896
- annotationsAsObjects: false
908
+ annotationsAsObjects: false,
909
+ getAcceptedInsertChars
897
910
  })
898
911
  ),
899
912
  [