@teselagen/ove 0.7.29 → 0.7.30-beta.2

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/ove.css CHANGED
@@ -10689,6 +10689,14 @@ li.bp3-menu-divider:last-child {
10689
10689
  top: -2px;
10690
10690
  position: absolute;
10691
10691
  z-index: 10;
10692
+ display: flex;
10693
+ align-items: center;
10694
+ justify-content: center;
10695
+ font-family: monospace;
10696
+ }
10697
+
10698
+ .veMismatchLayer {
10699
+ z-index: 11; /* Ensure mismatch layers appear on top of normal selection layer */
10692
10700
  }
10693
10701
 
10694
10702
  .veCaret {
@@ -11847,9 +11855,14 @@ path.partWithSelectedTag {
11847
11855
  border-right-color: yellow !important;
11848
11856
  }
11849
11857
  .veSearchLayerActive {
11850
- stroke: red !important;
11851
- fill: red !important;
11852
- background: red !important;
11858
+ stroke: green !important;
11859
+ fill: green !important;
11860
+ background: green !important;
11861
+ }
11862
+ .veMismatchedBase {
11863
+ color: red !important;
11864
+ font-weight: bold;
11865
+ background-color: rgba(255, 0, 0, 0.3);
11853
11866
  }
11854
11867
  .notCaret.veSearchLayerBottomStrand:after {
11855
11868
  content: "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teselagen/ove",
3
- "version": "0.7.29",
3
+ "version": "0.7.30-beta.2",
4
4
  "main": "./src/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,19 +11,22 @@
11
11
  "./*": "./*"
12
12
  },
13
13
  "dependencies": {
14
- "@teselagen/sequence-utils": "0.3.31",
15
- "@teselagen/range-utils": "0.3.13",
16
- "@teselagen/ui": "0.8.6-beta.23",
17
- "@teselagen/file-utils": "0.3.20",
18
- "@teselagen/bio-parsers": "0.4.28",
19
14
  "@blueprintjs/core": "3.54.0",
20
15
  "@hello-pangea/dnd": "16.2.0",
21
16
  "@risingstack/react-easy-state": "^6.3.0",
17
+ "@teselagen/bio-parsers": "0.4.29-beta.1",
18
+ "@teselagen/file-utils": "0.3.20",
19
+ "@teselagen/range-utils": "0.3.14-beta.1",
22
20
  "@teselagen/react-list": "0.8.18",
21
+ "@teselagen/sequence-utils": "0.3.32-beta.2",
22
+ "@teselagen/ui": "0.8.6-beta.27",
23
+ "@use-gesture/react": "10.3.0",
24
+ "biomsa": "^0.2.4",
23
25
  "classnames": "^2.3.2",
24
- "color": "^3.2.1",
26
+ "color": "^5.0.0",
25
27
  "combokeys": "^3.0.1",
26
28
  "copy-to-clipboard": "^3.3.1",
29
+ "cypress-real-events": "^1.13.0",
27
30
  "deep-equal": "^1.1.1",
28
31
  "dom-to-image": "^2.6.0",
29
32
  "downloadjs": "^1.4.7",
@@ -58,11 +61,11 @@
58
61
  "to-regex-range": "5.0.1",
59
62
  "use-debounce": "^8.0.4",
60
63
  "validate.io-nonnegative-integer-array": "^1.0.1",
61
- "cypress-real-events": "^1.13.0",
62
- "biomsa": "^0.2.4",
63
- "shortid": "2.2.16",
64
- "@use-gesture/react": "10.3.0",
65
- "@playwright/test": "^1.44.1"
64
+ "nanoid": "5.1.5"
65
+ },
66
+ "volta": {
67
+ "node": "18.18.0",
68
+ "yarn": "1.22.21"
66
69
  },
67
70
  "license": "MIT"
68
71
  }
@@ -4,6 +4,7 @@ export const toggleIsInline: import('redux-act').ComplexActionCreator1<any, any,
4
4
  export const updateSearchText: import('redux-act').ComplexActionCreator1<any, any, any>;
5
5
  export const updateAmbiguousOrLiteral: import('redux-act').ComplexActionCreator1<any, any, any>;
6
6
  export const updateDnaOrAA: import('redux-act').ComplexActionCreator1<any, any, any>;
7
+ export const updateMismatchesAllowed: import('redux-act').ComplexActionCreator1<any, any, any>;
7
8
  export const updateMatchNumber: import('redux-act').ComplexActionCreator1<any, any, any>;
8
9
  declare const _default: {
9
10
  (newState: {} | undefined, action: any): any;
@@ -7,7 +7,7 @@ declare const _default: ((state: any) => {
7
7
  forward: any;
8
8
  annotationTypePlural: string;
9
9
  isOrf: boolean;
10
- id: any;
10
+ id: string;
11
11
  }[]) & import('reselect').OutputSelectorFields<(args_0: any, args_1: any, args_2: any, args_3: any) => {
12
12
  start: number;
13
13
  end: number;
@@ -17,7 +17,7 @@ declare const _default: ((state: any) => {
17
17
  forward: any;
18
18
  annotationTypePlural: string;
19
19
  isOrf: boolean;
20
- id: any;
20
+ id: string;
21
21
  }[], {
22
22
  clearCache: () => void;
23
23
  }> & {
@@ -1,4 +1,4 @@
1
- declare const _default: ((state: any) => any[]) & import('reselect').OutputSelectorFields<(args_0: any, args_1: any, args_2: any, args_3: any, args_4: any, args_5: any, args_6: any, args_7: any) => any[], {
1
+ declare const _default: ((state: any) => any[]) & import('reselect').OutputSelectorFields<(args_0: any, args_1: any, args_2: any, args_3: any, args_4: any, args_5: any, args_6: any, args_7: any, args_8: any) => any[], {
2
2
  clearCache: () => void;
3
3
  }> & {
4
4
  clearCache: () => void;
@@ -7,7 +7,7 @@ declare const _default: ((state: any) => any) & import('reselect').OutputSelecto
7
7
  forward: any;
8
8
  annotationTypePlural: string;
9
9
  isOrf: boolean;
10
- id: any;
10
+ id: string;
11
11
  }[], args_4: any, args_5: any, args_6: any[], args_7: any, args_8: any, args_9: any, args_10: any, args_11: any, args_12: any) => any, {
12
12
  clearCache: () => void;
13
13
  }> & {
@@ -4,7 +4,7 @@
4
4
  import { unparse } from "papaparse";
5
5
  import pluralize from "pluralize";
6
6
  import { SubmissionError, reduxForm } from "redux-form";
7
- import shortid from "shortid";
7
+ import { nanoid } from "nanoid";
8
8
  import CreateAnnotationsPage from "./CreateAnnotationsPage";
9
9
  import { formName } from "./constants";
10
10
  import { AutoAnnotateBpMatchingDialog } from "./AutoAnnotateBpMatchingDialog";
@@ -33,7 +33,7 @@ import {
33
33
  } from "@teselagen/ui";
34
34
  import { startCase } from "lodash-es";
35
35
  import withEditorProps from "./withEditorProps";
36
- import { useEffect, useState } from "react";
36
+ import React, { useEffect, useState } from "react";
37
37
  import { Colors, Tab, Tabs } from "@blueprintjs/core";
38
38
  import { typeField } from "./helperComponents/PropertiesDialog/typeField";
39
39
 
@@ -403,7 +403,7 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`,
403
403
  if (ann.matchType === "protein") {
404
404
  ann.sequence = convertProteinSeqToDNAIupac(ann.sequence);
405
405
  }
406
- const id = shortid();
406
+ const id = nanoid();
407
407
  annotationsToCheckById[id] = {
408
408
  ...ann,
409
409
  sequence: ann.isRegex
@@ -428,7 +428,7 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`,
428
428
  ...annotationsToCheckById[a.id],
429
429
  ...a,
430
430
  forward: a.strand !== -1,
431
- id: shortid()
431
+ id: nanoid()
432
432
  };
433
433
  toRet.color =
434
434
  toRet.color || getFeatureToColorMap()[toRet.type];
@@ -1,6 +1,5 @@
1
1
  import { compose } from "redux";
2
2
  import pluralize from "pluralize";
3
-
4
3
  import { formName } from "./constants";
5
4
  import { typeField } from "./helperComponents/PropertiesDialog/typeField";
6
5
  import {
@@ -9,7 +8,7 @@ import {
9
8
  withSelectTableRecords,
10
9
  withSelectedEntities
11
10
  } from "@teselagen/ui";
12
- import { useEffect } from "react";
11
+ import React, { useEffect } from "react";
13
12
  import { hideDialog } from "./GlobalDialogUtils";
14
13
  import { startCase } from "lodash-es";
15
14
  import { tidyUpAnnotation } from "@teselagen/sequence-utils";
@@ -89,9 +89,14 @@
89
89
  border-right-color: yellow !important;
90
90
  }
91
91
  .veSearchLayerActive {
92
- stroke: red !important;
93
- fill: red !important;
94
- background: red !important;
92
+ stroke: green !important;
93
+ fill: green !important;
94
+ background: green !important;
95
+ }
96
+ .veMismatchedBase {
97
+ color: red !important;
98
+ font-weight: bold;
99
+ background-color: rgba(255, 0, 0, 0.3);
95
100
  }
96
101
  .notCaret.veSearchLayerBottomStrand:after {
97
102
  content: "";
@@ -6,7 +6,8 @@ import {
6
6
  Popover,
7
7
  Position,
8
8
  TextArea,
9
- Tooltip
9
+ Tooltip,
10
+ NumericInput
10
11
  } from "@blueprintjs/core";
11
12
  import withEditorProps from "../withEditorProps";
12
13
  import { MAX_MATCHES_DISPLAYED } from "../constants/findToolConstants";
@@ -50,6 +51,7 @@ export class FindBar extends React.Component {
50
51
  annotationVisibilityShow,
51
52
  updateAmbiguousOrLiteral,
52
53
  updateDnaOrAA,
54
+ updateMismatchesAllowed,
53
55
  updateMatchNumber: _updateMatchNumber,
54
56
  selectionLayerUpdate,
55
57
  annotationSearchMatches,
@@ -63,6 +65,7 @@ export class FindBar extends React.Component {
63
65
  ambiguousOrLiteral,
64
66
  matchesTotal = 0,
65
67
  matchNumber = 0,
68
+ mismatchesAllowed = 0,
66
69
  isInline
67
70
  } = findTool;
68
71
  const updateMatchNumber = (...args) => {
@@ -139,6 +142,33 @@ export class FindBar extends React.Component {
139
142
  </div>
140
143
  </InfoHelper>
141
144
  </div>,
145
+ <div
146
+ key="mismatchesAllowed"
147
+ style={{ marginTop: "8px", display: "flex", alignItems: "center" }}
148
+ >
149
+ <label style={{ marginRight: "8px" }}>Mismatches Allowed:</label>
150
+ <NumericInput
151
+ min={0}
152
+ max={10}
153
+ className={"tg-mismatches-allowed-input"}
154
+ style={{ width: "60px" }}
155
+ value={mismatchesAllowed}
156
+ disabled={dnaOrAA !== "DNA" || ambiguousOrLiteral !== "LITERAL"}
157
+ onValueChange={value => {
158
+ const newValue = Math.max(0, parseInt(value, 10) || 0);
159
+ updateMismatchesAllowed(newValue);
160
+ }}
161
+ />
162
+ <InfoHelper style={{ marginLeft: 10 }}>
163
+ <div>
164
+ Number of mismatches allowed when searching DNA sequences with
165
+ literal matching.
166
+ <br />
167
+ <br />
168
+ Higher values may slow down search performance.
169
+ </div>
170
+ </InfoHelper>
171
+ </div>,
142
172
  <Switch
143
173
  key="highlightall"
144
174
  checked={highlightAll}
@@ -152,6 +182,7 @@ export class FindBar extends React.Component {
152
182
  Highlight All
153
183
  </Tooltip>
154
184
  </Switch>,
185
+
155
186
  ...(isMobile()
156
187
  ? []
157
188
  : [
@@ -1,4 +1,4 @@
1
- import shortid from "shortid";
1
+ import { nanoid } from "nanoid";
2
2
 
3
3
  import { cloneDeep, startCase } from "lodash-es";
4
4
  import { convertRangeTo1Based } from "@teselagen/range-utils";
@@ -36,7 +36,7 @@ export function showDialog({
36
36
  dialogHolder.CustomModalComponent = ModalComponent;
37
37
  dialogHolder.props = props;
38
38
  dialogHolder.overrideName = overrideName;
39
- dialogHolder.setUniqKeyToForceRerender(shortid());
39
+ dialogHolder.setUniqKeyToForceRerender(nanoid());
40
40
  }
41
41
  export function hideDialog() {
42
42
  delete dialogHolder.dialogType;
@@ -149,7 +149,8 @@ function SelectionLayer(props) {
149
149
  )
150
150
  ];
151
151
  }
152
- return [
152
+ // Create base selection layer element
153
+ const selectionElement = (
153
154
  <div
154
155
  onClick={_onClick}
155
156
  title={selectionMessage}
@@ -168,9 +169,46 @@ function SelectionLayer(props) {
168
169
  background: color || topLevelColor,
169
170
  height
170
171
  }}
171
- />,
172
- ...caretSvgs
173
- ];
172
+ ></div>
173
+ );
174
+
175
+ // Generate mismatch sub-regions if mismatchPositions exist
176
+ let mismatchElements = [];
177
+ if (
178
+ selectionLayer.mismatchPositions &&
179
+ selectionLayer.mismatchPositions.length > 0
180
+ ) {
181
+ // Calculate relative mismatch positions within this overlap
182
+ const relativeToOverlap = selectionLayer.mismatchPositions
183
+ .filter(pos => {
184
+ // Adjust position based on overlap's relation to the original selection layer
185
+ const absPos = pos + selectionLayer.start;
186
+ return absPos >= overlap.start && absPos <= overlap.end;
187
+ })
188
+ .map(pos => {
189
+ // Convert to position relative to this overlap segment
190
+ return pos - (overlap.start - selectionLayer.start);
191
+ });
192
+
193
+ // Create a red highlight for each mismatch position
194
+ mismatchElements = relativeToOverlap.map((pos, i) => (
195
+ <div
196
+ key={`${key}-mismatch-${i}`}
197
+ className="veSelectionLayer veMismatchLayer notCaret"
198
+ style={{
199
+ width: charWidth,
200
+ left: leftMargin + xStart + pos * charWidth,
201
+ top: 0,
202
+ height: height || "100%",
203
+ background: "red",
204
+ position: "absolute",
205
+ opacity: 0.5
206
+ }}
207
+ ></div>
208
+ ));
209
+ }
210
+
211
+ return [selectionElement, ...mismatchElements, ...caretSvgs];
174
212
  });
175
213
  } else {
176
214
  return null;
@@ -6,6 +6,14 @@
6
6
  top: -2px;
7
7
  position: absolute;
8
8
  z-index: 10;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ font-family: monospace;
13
+ }
14
+
15
+ .veMismatchLayer {
16
+ z-index: 11; /* Ensure mismatch layers appear on top of normal selection layer */
9
17
  }
10
18
 
11
19
  .veCaret {
@@ -10,7 +10,7 @@ import {
10
10
  import { reduxForm, FieldArray } from "redux-form";
11
11
  import { anyToJson } from "@teselagen/bio-parsers";
12
12
  import { flatMap } from "lodash-es";
13
- import uniqid from "shortid";
13
+ import { nanoid } from "nanoid";
14
14
  import { cloneDeep } from "lodash-es";
15
15
  import classNames from "classnames";
16
16
 
@@ -155,7 +155,7 @@ class AlignmentTool extends React.Component {
155
155
  }
156
156
 
157
157
  hideModal();
158
- const alignmentId = uniqid();
158
+ const alignmentId = nanoid();
159
159
  // const alignmentIdMismatches = uniqid();
160
160
  createNewAlignment({
161
161
  id: alignmentId,
@@ -0,0 +1,103 @@
1
+ /* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
2
+ import { parse } from "papaparse";
3
+
4
+ export const allowedCsvFileTypes = [".csv", ".txt", ".xlsx"];
5
+
6
+ export const isZipFile = file => {
7
+ const type = file.mimetype || file.type;
8
+ return type === "application/zip" || type === "application/x-zip-compressed";
9
+ };
10
+
11
+ export const getExt = file => file.name.split(".").pop();
12
+ export const isExcelFile = file => getExt(file) === "xlsx";
13
+ export const isCsvFile = file => getExt(file) === "csv";
14
+ export const isTextFile = file => ["text", "txt"].includes(getExt(file));
15
+
16
+ const defaultCsvParserOptions = {
17
+ header: true,
18
+ skipEmptyLines: "greedy",
19
+ trimHeaders: true
20
+ // delimiter: ","
21
+ };
22
+
23
+ export const parseCsvFile = (csvFile, parserOptions = {}) => {
24
+ return new Promise((resolve, reject) => {
25
+ parse(csvFile.originFileObj, {
26
+ ...defaultCsvParserOptions,
27
+ complete: results => {
28
+ if (results && results.errors && results.errors.length) {
29
+ return reject("Error in csv: " + JSON.stringify(results.errors));
30
+ }
31
+ resolve(results);
32
+ },
33
+ error: error => {
34
+ reject(error);
35
+ },
36
+ ...parserOptions
37
+ });
38
+ });
39
+ };
40
+
41
+ export const parseCsvString = (csvString, parserOptions = {}) => {
42
+ return parse(csvString, { ...defaultCsvParserOptions, ...parserOptions });
43
+ };
44
+
45
+ export const cleanCommaSeparatedCell = cellData =>
46
+ (cellData || "")
47
+ .split(",")
48
+ .map(n => n.trim())
49
+ .filter(n => n);
50
+
51
+ /**
52
+ * Because the csv rows might not have the same header keys in some cases (extended properties)
53
+ * this function will make sure that each row will have all headers so that the export
54
+ * does not drop fields
55
+ * @param {*} rows
56
+ */
57
+ export const cleanCsvExport = rows => {
58
+ const allHeaders = [];
59
+ rows.forEach(row => {
60
+ Object.keys(row).forEach(header => {
61
+ if (!allHeaders.includes(header)) {
62
+ allHeaders.push(header);
63
+ }
64
+ });
65
+ });
66
+ rows.forEach(row => {
67
+ allHeaders.forEach(header => {
68
+ row[header] = row[header] || "";
69
+ });
70
+ });
71
+ return rows;
72
+ };
73
+
74
+ export const validateCSVRequiredHeaders = (
75
+ fields,
76
+ requiredHeaders,
77
+ filename
78
+ ) => {
79
+ const missingRequiredHeaders = requiredHeaders.filter(field => {
80
+ return !fields.includes(field);
81
+ });
82
+ if (missingRequiredHeaders.length) {
83
+ const name = filename ? `The file ${filename}` : "CSV file";
84
+ return `${name} is missing required headers. (${missingRequiredHeaders.join(
85
+ ", "
86
+ )})`;
87
+ }
88
+ };
89
+
90
+ export const validateCSVRow = (row, requiredHeaders, index) => {
91
+ const missingRequiredFields = requiredHeaders.filter(field => !row[field]);
92
+ if (missingRequiredFields.length) {
93
+ if (missingRequiredFields.length === 1) {
94
+ return `Row ${index + 1} is missing the required field "${
95
+ missingRequiredFields[0]
96
+ }"`;
97
+ } else {
98
+ return `Row ${
99
+ index + 1
100
+ } is missing these required fields: ${missingRequiredFields.join(", ")}`;
101
+ }
102
+ }
103
+ };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import uuid from "shortid";
2
+ import { nanoid } from "nanoid";
3
3
 
4
4
  import { reduxForm } from "redux-form";
5
5
 
@@ -58,7 +58,7 @@ class MergeFeaturesDialog extends React.Component {
58
58
  upsertFeature(
59
59
  {
60
60
  ...feat1,
61
- id: uuid(),
61
+ id: nanoid(),
62
62
  start: start - 1,
63
63
  end: end - 1,
64
64
  name
@@ -3,7 +3,7 @@ import {
3
3
  condensePairwiseAlignmentDifferences
4
4
  } from "@teselagen/sequence-utils";
5
5
  import { convertBasePosTraceToPerBpTrace } from "@teselagen/bio-parsers";
6
- import shortid from "shortid";
6
+ import { nanoid } from "nanoid";
7
7
 
8
8
  import addDashesForMatchStartAndEndForTracks from "./utils/addDashesForMatchStartAndEndForTracks";
9
9
 
@@ -149,7 +149,7 @@ export default (state = {}, { payload = {}, type }) => {
149
149
  if (type === "UPSERT_ALIGNMENT_RUN") {
150
150
  const { id } = payload;
151
151
  const payloadToUse = {
152
- stateTrackingId: state[id]?.stateTrackingId ? shortid() : "initialLoadId",
152
+ stateTrackingId: state[id]?.stateTrackingId ? nanoid() : "initialLoadId",
153
153
  alignmentType: state[id]?.alignmentType,
154
154
  ...payload,
155
155
  //assign default visibilities
@@ -13,6 +13,7 @@ export const updateAmbiguousOrLiteral = createAction(
13
13
  "updateAmbiguousOrLiteral"
14
14
  );
15
15
  export const updateDnaOrAA = createAction("updateDnaOrAA");
16
+ export const updateMismatchesAllowed = createAction("updateMismatchesAllowed");
16
17
  export const updateMatchNumber = createAction("updateMatchNumber");
17
18
 
18
19
  // ------------------------------------
@@ -53,6 +54,13 @@ export default createMergedDefaultStateReducer(
53
54
  dnaOrAA: payload
54
55
  };
55
56
  },
57
+ [updateMismatchesAllowed]: (state, payload) => {
58
+ return {
59
+ ...state,
60
+ matchNumber: 0,
61
+ mismatchesAllowed: payload
62
+ };
63
+ },
56
64
  [updateSearchText]: (state, payload) => {
57
65
  return {
58
66
  ...state,
@@ -74,6 +82,7 @@ export default createMergedDefaultStateReducer(
74
82
  dnaOrAA: "DNA",
75
83
  ambiguousOrLiteral: "LITERAL",
76
84
  highlightAll: false,
85
+ mismatchesAllowed: 0,
77
86
  matchNumber: 0
78
87
  }
79
88
  );
@@ -1,6 +1,6 @@
1
1
  import deepEqual from "deep-equal";
2
2
  import { tidyUpSequenceData } from "@teselagen/sequence-utils";
3
- import uuid from "shortid";
3
+ import { nanoid } from "nanoid";
4
4
 
5
5
  import createAction from "../utils/createMetaAction";
6
6
  import features from "./features";
@@ -75,7 +75,7 @@ export default function (state, action) {
75
75
  return {
76
76
  // ...cloneDeep(newState),
77
77
  ...newState,
78
- stateTrackingId: newState.stateTrackingId ? uuid() : "initialLoadId"
78
+ stateTrackingId: newState.stateTrackingId ? nanoid() : "initialLoadId"
79
79
  };
80
80
  }
81
81
  }
@@ -1,5 +1,5 @@
1
1
  import omit from "lodash/omit";
2
- import uuid from "shortid";
2
+ import { nanoid } from "nanoid";
3
3
 
4
4
  // ------------------------------------
5
5
  // Reducer
@@ -10,7 +10,7 @@ export default function upsertDeleteActionGenerator(
10
10
  ) {
11
11
  return {
12
12
  [upsertAction]: (state, payload) => {
13
- const idToUse = payload.id || uuid();
13
+ const idToUse = payload.id || nanoid();
14
14
  return {
15
15
  ...state,
16
16
  [idToUse]: { ...(state[idToUse] || {}), ...payload, id: idToUse }
@@ -1,4 +1,4 @@
1
- import shortid from "shortid";
1
+ import { nanoid } from "nanoid";
2
2
  import circularSelector from "./circularSelector";
3
3
  import sequenceSelector from "./sequenceSelector";
4
4
  import restrictionEnzymesSelector from "./restrictionEnzymesSelector";
@@ -20,7 +20,7 @@ function cutsitesSelector(sequence, circular, enzymeList, cutsiteLabelColors) {
20
20
  const cutsitesForEnzyme = cutsitesByName[enzymeName];
21
21
  cutsitesForEnzyme.forEach(function (cutsite) {
22
22
  const numberOfCuts = cutsitesByName[enzymeName].length;
23
- const uniqueId = shortid();
23
+ const uniqueId = nanoid();
24
24
  cutsite.id = uniqueId;
25
25
  cutsite.numberOfCuts = numberOfCuts;
26
26
  cutsite.annotationType = "cutsite";
@@ -1,4 +1,7 @@
1
- import { findSequenceMatches } from "@teselagen/sequence-utils";
1
+ import {
2
+ findSequenceMatches,
3
+ findApproxMatches
4
+ } from "@teselagen/sequence-utils";
2
5
  import sequenceSelector from "./sequenceSelector";
3
6
  import { createSelector } from "reselect";
4
7
  import circularSelector from "./circularSelector";
@@ -11,7 +14,8 @@ function searchLayersSelector(
11
14
  ambiguousOrLiteral,
12
15
  dnaOrAA,
13
16
  isProtein,
14
- proteinSequence
17
+ proteinSequence,
18
+ mismatchesAllowed
15
19
  ) {
16
20
  if (!searchString || !isOpen) {
17
21
  return [];
@@ -40,6 +44,39 @@ function searchLayersSelector(
40
44
  end: end * 3 + 2
41
45
  }));
42
46
  }
47
+
48
+ // Use findApproxMatches when literal matching DNA with mismatches allowed
49
+ if (
50
+ dnaOrAA === "DNA" &&
51
+ ambiguousOrLiteral === "LITERAL" &&
52
+ mismatchesAllowed > 0
53
+ ) {
54
+ const approxMatches = findApproxMatches(
55
+ searchString,
56
+ sequence,
57
+ mismatchesAllowed,
58
+ isCircular
59
+ );
60
+ // Convert approximate matches to the format expected by the application
61
+ const matches = approxMatches
62
+ .map(match => ({
63
+ start: match.index,
64
+ end: match.index + match.match.length - 1,
65
+ matchedString: match.match,
66
+ mismatchPositions: match.mismatchPositions,
67
+ numMismatches: match.numMismatches,
68
+ isSearchLayer: true,
69
+ forward: true
70
+ }))
71
+ .sort((a, b) => a.start - b.start);
72
+
73
+ return matches.map(match => ({
74
+ ...match,
75
+ className: "veSearchLayer"
76
+ }));
77
+ }
78
+
79
+ // Use regular findSequenceMatches for all other cases
43
80
  const matches = findSequenceMatches(sequence, searchString, {
44
81
  isCircular,
45
82
  isAmbiguous: ambiguousOrLiteral === "AMBIGUOUS",
@@ -67,5 +104,6 @@ export default createSelector(
67
104
  state => state.findTool && state.findTool.dnaOrAA,
68
105
  state => state.sequenceData.isProtein,
69
106
  state => state.sequenceData.proteinSequence,
107
+ state => state.findTool && state.findTool.mismatchesAllowed,
70
108
  searchLayersSelector
71
109
  );