@opencloning/ui 1.1.2 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#595](https://github.com/manulera/OpenCloning_frontend/pull/595) [`1b28cc5`](https://github.com/manulera/OpenCloning_frontend/commit/1b28cc5852460a072982dc529b58fc9607fae21f) Thanks [@manulera](https://github.com/manulera)! - Minor improvements and bug fixes:
8
+
9
+ - include name of tracks in alignment + update ove to display correct Track Properties table
10
+ - fix display main sequence when alignments are present
11
+ - change default minimum hib length to 14 for primer design
12
+ - Gibson primer design: default to circular assembly, force circular for single input assemblies
13
+ - Gibson primer design: make product sequence preview circular when assembly is circular
14
+ - Primer design: in circular assemblies of one fragment only, display the spacer before the fragment in the preview.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies []:
19
+ - @opencloning/store@1.2.0
20
+ - @opencloning/utils@1.2.0
21
+
3
22
  ## 1.1.2
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -24,10 +24,10 @@
24
24
  "@emotion/styled": "^11.14.0",
25
25
  "@mui/icons-material": "^5.15.17",
26
26
  "@mui/material": "^5.15.17",
27
- "@opencloning/store": "1.1.2",
28
- "@opencloning/utils": "1.1.2",
27
+ "@opencloning/store": "1.2.0",
28
+ "@opencloning/utils": "1.2.0",
29
29
  "@teselagen/bio-parsers": "^0.4.32",
30
- "@teselagen/ove": "^0.8.18",
30
+ "@teselagen/ove": "^0.8.30",
31
31
  "@teselagen/range-utils": "^0.3.13",
32
32
  "@teselagen/sequence-utils": "^0.3.35",
33
33
  "@zip.js/zip.js": "^2.7.62",
@@ -65,6 +65,8 @@ function MainSequenceEditor() {
65
65
  (state) => {
66
66
  const history = state.VectorEditor.mainEditor?.sequenceDataHistory;
67
67
  if (!history) return false;
68
+ const sequenceId = state.VectorEditor.mainEditor?.sequenceData?.id;
69
+ if (sequenceId === 'opencloning_primer_design_product') return false;
68
70
  return state.cloning.mainSequenceId && Object.keys(history).length > 0 && history.future.length === 0;
69
71
  }
70
72
  );
@@ -138,7 +138,6 @@ function AssemblerComponent({ data, categories }) {
138
138
  {index === 0 && item.category !== '' && (
139
139
  <AssemblerLink overhang={leftOverhang} />
140
140
  )}
141
- <AssemblerPart />
142
141
  <Box sx={{ width: '250px', border: 3, borderColor, borderRadius: 4, p: 2 }}>
143
142
  <FormControl fullWidth sx={{ mb: 2 }}>
144
143
  <InputLabel>Category</InputLabel>
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { batch, useDispatch, useSelector, useStore } from 'react-redux';
3
3
  import { updateEditor } from '@teselagen/ove';
4
4
  import { isEqual } from 'lodash-es';
@@ -18,19 +18,28 @@ function changeValueAtIndex(current, index, newValue) {
18
18
  const PrimerDesignContext = React.createContext();
19
19
 
20
20
  export function PrimerDesignProvider({ children, designType, sequenceIds, primerDesignSettings, steps }) {
21
- let templateSequenceIds = sequenceIds;
22
- if (designType === 'homologous_recombination' || designType === 'gateway_bp') {
23
- templateSequenceIds = sequenceIds.slice(0, 1);
24
- }
21
+
22
+ const templateSequenceIds = React.useMemo(() => {
23
+ if (designType === 'homologous_recombination' || designType === 'gateway_bp') {
24
+ return sequenceIds.slice(0, 1);
25
+ }
26
+ return sequenceIds;
27
+ }, [sequenceIds, designType]);
28
+
29
+ // Compute initial values based on design type (props don't change, so compute once)
30
+ const initialFragmentOrientationsLength = templateSequenceIds.length;
31
+ const initialCircularAssembly = designType === 'gibson_assembly';
32
+ const initialSpacersLength = initialCircularAssembly ? initialFragmentOrientationsLength : initialFragmentOrientationsLength + 1;
25
33
 
26
34
  const [primers, setPrimers] = useState([]);
27
35
  const [rois, setRois] = useState(Array(sequenceIds.length).fill(null));
28
36
  const [error, setError] = useState('');
29
37
  const [selectedTab, setSelectedTab] = useState(0);
30
38
  const [sequenceProduct, setSequenceProduct] = useState(null);
31
- const [fragmentOrientations, setFragmentOrientations] = useState(Array(templateSequenceIds.length).fill('forward'));
32
- const [circularAssembly, setCircularAssembly] = useState(false);
33
- const [spacers, setSpacers] = useState(Array(templateSequenceIds.length + 1).fill(''));
39
+ const [fragmentOrientations, setFragmentOrientations] = useState(Array(initialFragmentOrientationsLength).fill('forward'));
40
+ const [circularAssembly, setCircularAssembly] = useState(initialCircularAssembly);
41
+ const [spacers, setSpacers] = useState(Array(initialSpacersLength).fill(''));
42
+ const sequenceProductTimeoutRef = React.useRef();
34
43
 
35
44
  const spacersAreValid = React.useMemo(() => spacers.every((spacer) => !stringIsNotDNA(spacer)), [spacers]);
36
45
  const sequenceNames = useSelector((state) => sequenceIds.map((id) => state.cloning.teselaJsonCache[id].name), isEqual);
@@ -44,7 +53,7 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
44
53
  const { setMainSequenceId, addPrimersToPCRSource, setCurrentTab } = cloningActions;
45
54
  const httpClient = useHttpClient();
46
55
 
47
- const getSubmissionPreventedMessage = () => {
56
+ const submissionPreventedMessage = React.useMemo(() => {
48
57
  if (rois.some((region) => region === null)) {
49
58
  return 'Not all regions have been selected';
50
59
  } if (primerDesignSettings.error) {
@@ -53,71 +62,78 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
53
62
  return 'Spacer sequences not valid';
54
63
  }
55
64
  return '';
56
- };
57
-
58
- const submissionPreventedMessage = getSubmissionPreventedMessage();
65
+ }, [rois, primerDesignSettings.error, spacers]);
59
66
 
60
67
  React.useEffect(() => {
61
- let newSequenceProduct = null;
62
- if (submissionPreventedMessage === '') {
63
- const { teselaJsonCache } = store.getState().cloning;
64
- const sequences = sequenceIds.map((id) => teselaJsonCache[id]);
65
- if (designType === 'simple_pair' || designType === 'restriction_ligation') {
66
- const enzymeSpacers = designType === 'restriction_ligation' ? primerDesignSettings.enzymeSpacers : ['', ''];
67
- const extendedSpacers = [enzymeSpacers[0] + spacers[0], spacers[1] + enzymeSpacers[1]];
68
- newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, extendedSpacers, circularAssembly, 'primer tail');
69
- newSequenceProduct.name = 'PCR product';
70
- } else if (designType === 'gibson_assembly') {
71
- newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, spacers, circularAssembly);
72
- newSequenceProduct.name = 'Gibson Assembly product';
73
- } else if (designType === 'homologous_recombination') {
74
- newSequenceProduct = simulateHomologousRecombination(sequences[0], sequences[1], rois, fragmentOrientations[0] === 'reverse', spacers);
75
- newSequenceProduct.name = 'Homologous recombination product';
76
- } else if (designType === 'gateway_bp') {
77
- newSequenceProduct = joinSequencesIntoSingleSequence([sequences[0]], [rois[0].selectionLayer], fragmentOrientations, spacers, false, 'primer tail');
78
- newSequenceProduct.name = 'PCR product';
79
- const { knownCombination } = primerDesignSettings;
80
- const leftFeature = {
81
- start: knownCombination.translationFrame[0],
82
- end: spacers[0].length - 1,
83
- type: 'CDS',
84
- name: 'translation frame',
85
- strand: 1,
86
- forward: true,
87
- };
88
- const nbAas = Math.floor((spacers[1].length - knownCombination.translationFrame[1]) / 3);
89
- const rightStart = newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - nbAas * 3;
90
- const rightFeature = {
91
- start: rightStart,
92
- end: newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - 1,
93
- type: 'CDS',
94
- name: 'translation frame',
95
- strand: 1,
96
- forward: true,
97
- };
98
- newSequenceProduct.features.push(leftFeature);
99
- newSequenceProduct.features.push(rightFeature);
100
- setSequenceProduct(newSequenceProduct);
101
- } else if (designType === 'ebic') {
102
- newSequenceProduct = ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
103
- setSequenceProduct(newSequenceProduct);
68
+ // Clear any existing timeout
69
+ clearTimeout(sequenceProductTimeoutRef.current);
70
+
71
+ // Debounce the heavy calculation
72
+ sequenceProductTimeoutRef.current = setTimeout(() => {
73
+ let newSequenceProduct = null;
74
+ if (submissionPreventedMessage === '') {
75
+ const { teselaJsonCache } = store.getState().cloning;
76
+ const sequences = sequenceIds.map((id) => teselaJsonCache[id]);
77
+ if (designType === 'simple_pair' || designType === 'restriction_ligation') {
78
+ const enzymeSpacers = designType === 'restriction_ligation' ? primerDesignSettings.enzymeSpacers : ['', ''];
79
+ const extendedSpacers = [enzymeSpacers[0] + spacers[0], spacers[1] + enzymeSpacers[1]];
80
+ newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, extendedSpacers, circularAssembly, 'primer tail');
81
+ newSequenceProduct.name = 'PCR product';
82
+ } else if (designType === 'gibson_assembly') {
83
+ newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, spacers, circularAssembly);
84
+ newSequenceProduct.name = 'Gibson Assembly product';
85
+ } else if (designType === 'homologous_recombination') {
86
+ newSequenceProduct = simulateHomologousRecombination(sequences[0], sequences[1], rois, fragmentOrientations[0] === 'reverse', spacers);
87
+ newSequenceProduct.name = 'Homologous recombination product';
88
+ } else if (designType === 'gateway_bp') {
89
+ newSequenceProduct = joinSequencesIntoSingleSequence([sequences[0]], [rois[0].selectionLayer], fragmentOrientations, spacers, false, 'primer tail');
90
+ newSequenceProduct.name = 'PCR product';
91
+ const { knownCombination } = primerDesignSettings;
92
+ const leftFeature = {
93
+ start: knownCombination.translationFrame[0],
94
+ end: spacers[0].length - 1,
95
+ type: 'CDS',
96
+ name: 'translation frame',
97
+ strand: 1,
98
+ forward: true,
99
+ };
100
+ const nbAas = Math.floor((spacers[1].length - knownCombination.translationFrame[1]) / 3);
101
+ const rightStart = newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - nbAas * 3;
102
+ const rightFeature = {
103
+ start: rightStart,
104
+ end: newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - 1,
105
+ type: 'CDS',
106
+ name: 'translation frame',
107
+ strand: 1,
108
+ forward: true,
109
+ };
110
+ newSequenceProduct.features.push(leftFeature);
111
+ newSequenceProduct.features.push(rightFeature);
112
+ } else if (designType === 'ebic') {
113
+ newSequenceProduct = ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
114
+ }
104
115
  }
105
- }
106
- setSequenceProduct(newSequenceProduct);
107
- }, [rois, spacersAreValid, fragmentOrientations, circularAssembly, designType, spacers, primerDesignSettings]);
116
+ setSequenceProduct({...newSequenceProduct, id: 'opencloning_primer_design_product'});
117
+ }, 300);
118
+
119
+ // Cleanup timeout on unmount or when dependencies change
120
+ return () => {
121
+ clearTimeout(sequenceProductTimeoutRef.current);
122
+ };
123
+ }, [rois, spacersAreValid, fragmentOrientations, circularAssembly, designType, spacers, primerDesignSettings, sequenceIds, templateSequenceIds, store, submissionPreventedMessage]);
108
124
 
109
- const onCircularAssemblyChange = (event) => {
110
- setCircularAssembly(event.target.checked);
111
- if (event.target.checked) {
112
- // Remove the first spacer
125
+
126
+ React.useEffect(() => {
127
+ if (circularAssembly && spacers.length !== templateSequenceIds.length) {
113
128
  setSpacers((current) => current.slice(1));
114
- } else {
115
- // Add it again
129
+ }
130
+ if (!circularAssembly && spacers.length !== templateSequenceIds.length + 1) {
116
131
  setSpacers((current) => ['', ...current]);
117
132
  }
118
- };
133
+ }, [circularAssembly, spacers, templateSequenceIds.length]);
119
134
 
120
- const onSelectRegion = (index, selectedRegion, allowSinglePosition = false) => {
135
+
136
+ const onSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
121
137
  const { caretPosition } = selectedRegion;
122
138
  if (caretPosition === undefined) {
123
139
  setRois((c) => changeValueAtIndex(c, index, null));
@@ -133,39 +149,40 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
133
149
  }
134
150
  setRois((c) => changeValueAtIndex(c, index, null));
135
151
  return 'Select a region (not a single position) to amplify';
136
- };
152
+ }, [setRois]);
137
153
 
138
- const onTabChange = (event, newValue) => {
154
+ const onTabChange = useCallback((event, newValue) => {
139
155
  setSelectedTab(newValue);
140
156
  if (newValue < sequenceIds.length) {
141
157
  updateStoreEditor('mainEditor', sequenceIds[newValue]);
142
158
  dispatch(setMainSequenceId(sequenceIds[newValue]));
143
159
  } else if (newValue === sequenceIds.length) {
144
- updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || '', selectionLayer: {} });
160
+ // Don't update editor here - let the useEffect handle it when sequenceProduct is ready
161
+ // This avoids using stale data since sequenceProduct is debounced
145
162
  } else {
146
163
  updateStoreEditor('mainEditor', null);
147
164
  }
148
- };
165
+ }, [sequenceIds, updateStoreEditor, dispatch, setMainSequenceId, setSelectedTab]);
149
166
 
150
- const handleNext = () => {
167
+ const handleNext = useCallback(() => {
151
168
  onTabChange(null, selectedTab + 1);
152
- };
169
+ }, [onTabChange, selectedTab]);
153
170
 
154
- const handleBack = () => {
171
+ const handleBack = useCallback(() => {
155
172
  onTabChange(null, selectedTab - 1);
156
- };
173
+ }, [onTabChange, selectedTab]);
157
174
 
158
- const handleSelectRegion = (index, selectedRegion, allowSinglePosition = false) => {
175
+ const handleSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
159
176
  const regionError = onSelectRegion(index, selectedRegion, allowSinglePosition);
160
177
  if (!regionError) {
161
178
  handleNext();
162
179
  }
163
180
  return regionError;
164
- };
181
+ }, [onSelectRegion, handleNext]);
165
182
 
166
- const handleFragmentOrientationChange = (index, orientation) => {
183
+ const handleFragmentOrientationChange = useCallback((index, orientation) => {
167
184
  setFragmentOrientations((current) => changeValueAtIndex(current, index, orientation));
168
- };
185
+ }, [setFragmentOrientations]);
169
186
 
170
187
  // Focus on the right sequence when changing tabs
171
188
  useEffect(() => {
@@ -178,16 +195,16 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
178
195
 
179
196
  // Update the sequence product in the editor if in the last tab
180
197
  useEffect(() => {
181
- const timeoutId = setTimeout(() => {
182
- if (selectedTab === sequenceIds.length) {
183
- updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || {} });
184
- }
185
- }, 500);
198
+ if (selectedTab === sequenceIds.length) {
199
+ const timeoutId = setTimeout(() => {
200
+ updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || {}, selectionLayer: {} });
201
+ }, 100);
186
202
 
187
- return () => clearTimeout(timeoutId);
188
- }, [sequenceProduct, store]);
203
+ return () => clearTimeout(timeoutId);
204
+ }
205
+ }, [sequenceProduct, selectedTab, sequenceIds.length, store]);
189
206
 
190
- const designPrimers = async () => {
207
+ const designPrimers = useCallback(async () => {
191
208
  // Validate fragmentOrientations
192
209
  fragmentOrientations.forEach((orientation) => {
193
210
  if (orientation !== 'forward' && orientation !== 'reverse') {
@@ -277,9 +294,9 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
277
294
  setError(errorMessage);
278
295
  return true;
279
296
  }
280
- };
297
+ }, [fragmentOrientations, rois, sequenceIds, templateSequenceIds, designType, circularAssembly, primerDesignSettings, spacers, store, httpClient, backendRoute, handleNext]);
281
298
 
282
- const addPrimers = () => {
299
+ const addPrimers = useCallback(() => {
283
300
  const pcrSources = store.getState().cloning.sources.filter((source) => source.type === 'PCRSource');
284
301
  let usedPCRSources;
285
302
  if (designType === 'ebic') {
@@ -303,7 +320,7 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
303
320
  onTabChange(null, 0);
304
321
  document.getElementById(`source-${usedPCRSources[0].id}`)?.scrollIntoView();
305
322
  updateStoreEditor('mainEditor', null);
306
- };
323
+ }, [primers, dispatch, setMainSequenceId, setCurrentTab, onTabChange, updateStoreEditor, designType, templateSequenceIds, addPrimersToPCRSource, store]);
307
324
 
308
325
  const value = React.useMemo(() => ({
309
326
  primers,
@@ -318,7 +335,6 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
318
335
  handleSelectRegion,
319
336
  sequenceIds,
320
337
  fragmentOrientations,
321
- circularAssembly,
322
338
  spacers,
323
339
  setFragmentOrientations,
324
340
  setSpacers,
@@ -327,7 +343,8 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
327
343
  primerDesignSettings,
328
344
  submissionPreventedMessage,
329
345
  addPrimers,
330
- onCircularAssemblyChange,
346
+ circularAssembly,
347
+ setCircularAssembly,
331
348
  templateSequenceIds,
332
349
  templateSequenceNames,
333
350
  designType,
@@ -354,7 +371,9 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
354
371
  primerDesignSettings,
355
372
  submissionPreventedMessage,
356
373
  addPrimers,
374
+ setCircularAssembly,
357
375
  templateSequenceIds,
376
+ templateSequenceNames,
358
377
  designType,
359
378
  steps,
360
379
  ]);
@@ -19,7 +19,7 @@ function PrimerDesignGatewayBP({ donorVectorId, pcrSource }) {
19
19
  },
20
20
  ], [templateSequenceId, donorVectorId]);
21
21
 
22
- const primerDesignSettings = useGatewayPrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 20, target_tm: 55 });
22
+ const primerDesignSettings = useGatewayPrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 14, target_tm: 55 });
23
23
 
24
24
  return (
25
25
  <PrimerDesignProvider
@@ -10,9 +10,9 @@ export default function PrimerDesignGibsonAssembly({ pcrSources }) {
10
10
  ...templateSequencesIds.map((id, index) => (
11
11
  { label: `Seq ${id}`, selectOrientation: true }
12
12
  )),
13
- ], [pcrSources]);
13
+ ], [pcrSources, templateSequencesIds]);
14
14
 
15
- const primerDesignSettings = usePrimerDesignSettings({ homology_length: 35, minimal_hybridization_length: 20, target_tm: 55 });
15
+ const primerDesignSettings = usePrimerDesignSettings({ homology_length: 35, minimal_hybridization_length: 14, target_tm: 55 });
16
16
  return (
17
17
  <PrimerDesignProvider
18
18
  designType="gibson_assembly"
@@ -18,7 +18,7 @@ export default function PrimerDesignHomologousRecombination({ homologousRecombin
18
18
  },
19
19
  ], [templateSequenceId, homologousRecombinationTargetId]);
20
20
 
21
- const primerDesignSettings = usePrimerDesignSettings({ homology_length: 80, minimal_hybridization_length: 20, target_tm: 55 });
21
+ const primerDesignSettings = usePrimerDesignSettings({ homology_length: 80, minimal_hybridization_length: 14, target_tm: 55 });
22
22
  return (
23
23
  <PrimerDesignProvider
24
24
  designType="homologous_recombination"
@@ -12,7 +12,7 @@ function PrimerDesignRestriction({ pcrSource }) {
12
12
  { label: 'Amplified region' },
13
13
  ], []);
14
14
 
15
- const primerDesignSettings = useEnzymePrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 20, target_tm: 55 });
15
+ const primerDesignSettings = useEnzymePrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 14, target_tm: 55 });
16
16
 
17
17
  return (
18
18
  <PrimerDesignProvider designType="restriction_ligation" sequenceIds={sequenceIds} primerDesignSettings={primerDesignSettings} steps={steps}>
@@ -12,7 +12,7 @@ function PrimerDesignSimplePair({ pcrSource }) {
12
12
  { label: 'Amplified region' },
13
13
  ], []);
14
14
 
15
- const primerDesignSettings = usePrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 20, target_tm: 55 });
15
+ const primerDesignSettings = usePrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 14, target_tm: 55 });
16
16
 
17
17
  return (
18
18
  <PrimerDesignProvider designType="simple_pair" sequenceIds={sequenceIds} primerDesignSettings={primerDesignSettings} steps={steps}>
@@ -6,33 +6,16 @@ import { usePrimerDesign } from './PrimerDesignContext';
6
6
 
7
7
  function PrimerSpacerForm({ open = true }) {
8
8
  const { spacers, setSpacers, circularAssembly, templateSequenceNames, templateSequenceIds } = usePrimerDesign();
9
- const [localSpacers, setLocalSpacers] = React.useState(spacers);
10
- const timeoutRef = React.useRef();
11
-
12
- // Debounced upstream updates to avoid heavy re-rendering
13
- const handleSpacerChange = (index, value) => {
14
- setLocalSpacers((current) => current.map((spacer, i) => (i === index ? value : spacer)));
15
-
16
- // Clear any existing timeout
17
- clearTimeout(timeoutRef.current);
18
-
19
- // Set new timeout and store its ID in the ref
20
- timeoutRef.current = setTimeout(() => {
21
- setSpacers((current) => current.map((spacer, i) => (i === index ? value : spacer)));
22
- }, 500);
23
- };
24
-
25
- React.useEffect(() => {
26
- if (!localSpacers.every((spacer, index) => spacer === spacers[index])) {
27
- setLocalSpacers(spacers);
28
- }
29
- }, [spacers]);
30
9
 
31
10
  const fragmentCount = templateSequenceIds.length;
32
11
 
33
12
  const sequenceNamesWrapped = [...templateSequenceNames, templateSequenceNames[0]];
34
13
  const templateSequenceIdsWrapped = [...templateSequenceIds, templateSequenceIds[0]];
35
14
 
15
+ const handleSpacerChange = (index, value) => {
16
+ setSpacers((current) => current.map((spacer, i) => (i === index ? value : spacer)));
17
+ };
18
+
36
19
  const getSequenceName = (seqIndex) => {
37
20
  const name = sequenceNamesWrapped[seqIndex];
38
21
  const id = templateSequenceIdsWrapped[seqIndex];
@@ -55,7 +38,7 @@ function PrimerSpacerForm({ open = true }) {
55
38
  <CollapsableLabel label="Spacer sequences" className="primer-spacer-form" open={open}>
56
39
  <Box sx={{ pt: 1, width: '80%', margin: 'auto' }}>
57
40
  <Box>
58
- {localSpacers.map((spacer, index) => {
41
+ {spacers.map((spacer, index) => {
59
42
  const error = stringIsNotDNA(spacer) ? 'Invalid DNA sequence' : '';
60
43
  return (
61
44
  <FormControl key={index} fullWidth sx={{ mb: 2 }}>
@@ -9,7 +9,7 @@ import { usePrimerDesign } from './PrimerDesignContext';
9
9
  import RestrictionSpacerForm from './RestrictionSpacerForm';
10
10
 
11
11
  function TabPannelSettings() {
12
- const { error, templateSequenceIds, designType, selectedTab, sequenceIds, circularAssembly, onCircularAssemblyChange, designPrimers, primers, primerDesignSettings, submissionPreventedMessage } = usePrimerDesign();
12
+ const { error, templateSequenceIds, designType, selectedTab, sequenceIds, circularAssembly, setCircularAssembly, designPrimers, primers, primerDesignSettings, submissionPreventedMessage } = usePrimerDesign();
13
13
  return (
14
14
  <TabPanel value={selectedTab} index={sequenceIds.length}>
15
15
  <Box sx={{ width: '80%', margin: 'auto' }}>
@@ -32,12 +32,14 @@ function TabPannelSettings() {
32
32
  <FormControlLabel
33
33
  control={(
34
34
  <Checkbox
35
+ data-test="circular-assembly-checkbox"
36
+ disabled={templateSequenceIds.length === 1}
35
37
  checked={circularAssembly}
36
- onChange={onCircularAssemblyChange}
38
+ onChange={(e) => setCircularAssembly(e.target.checked)}
37
39
  name="circular-assembly"
38
40
  />
39
- )}
40
- label="Circular assembly"
41
+ )}
42
+ label={templateSequenceIds.length === 1 ? 'Circular assembly (only one sequence input)' : 'Circular assembly'}
41
43
  />
42
44
  </FormControl>
43
45
  </Box>
@@ -27,6 +27,8 @@ export default function useStoreEditor() {
27
27
  const pcrPrimers = getPCRPrimers(cloning, id);
28
28
  const alignmentFiles = cloning.files.filter((e) => e.sequence_id === id && e.file_type === 'Sequencing file');
29
29
  let { panelsShown } = store.getState().VectorEditor.mainEditor;
30
+ panelsShown = [[...panelsShown[0].filter((p) => p.id !== 'simpleAlignment')]]
31
+
30
32
  if (alignmentFiles.length > 0) {
31
33
  addAlignment(store, {
32
34
  id: 'simpleAlignment',
@@ -44,6 +46,7 @@ export default function useStoreEditor() {
44
46
  alignmentData: {
45
47
  // the alignmentData just needs the sequence < TODO this has to be changed to be the largest ---
46
48
  sequence: alignmentFiles[0].alignment[0],
49
+ name: sequenceData.name
47
50
  },
48
51
  },
49
52
  ...await Promise.all(alignmentFiles.map(async (aln) => {
@@ -75,6 +78,7 @@ export default function useStoreEditor() {
75
78
  sequenceData: {...alignmentSequenceData, name: aln.file_name},
76
79
  alignmentData: {
77
80
  sequence: aln.alignment[1],
81
+ name: aln.file_name
78
82
  },
79
83
  chromatogramData,
80
84
  };
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Version placeholder - replaced at publish time via prepack script
2
- export const version = "1.1.2";
2
+ export const version = "1.2.0";