@opencloning/ui 1.0.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.
Files changed (207) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +37 -0
  3. package/src/components/AppAlerts.jsx +19 -0
  4. package/src/components/CloningHistory.jsx +40 -0
  5. package/src/components/DataModelDisplayer.jsx +30 -0
  6. package/src/components/DescriptionEditor.jsx +68 -0
  7. package/src/components/DownloadCloningStrategyDialog.jsx +84 -0
  8. package/src/components/DownloadSequenceFileDialog.jsx +90 -0
  9. package/src/components/DragAndDropCloningHistoryWrapper.jsx +39 -0
  10. package/src/components/DraggableDialogPaper.jsx +16 -0
  11. package/src/components/EditSequenceNameDialog.jsx +90 -0
  12. package/src/components/ExternalServicesStatusCheck.jsx +92 -0
  13. package/src/components/HistoryLoadedDialog.jsx +49 -0
  14. package/src/components/LoadCloningHistoryWrapper.jsx +166 -0
  15. package/src/components/MainSequenceCheckBox.jsx +83 -0
  16. package/src/components/MainSequenceEditor.jsx +165 -0
  17. package/src/components/NetworkNode.jsx +159 -0
  18. package/src/components/NetworkTree.css +127 -0
  19. package/src/components/ObjectTable.jsx +24 -0
  20. package/src/components/OpenCloning.jsx +102 -0
  21. package/src/components/OverhangsDisplay.jsx +25 -0
  22. package/src/components/SequenceEditor.jsx +120 -0
  23. package/src/components/SequenceTab.jsx +14 -0
  24. package/src/components/TemplateSequence.jsx +38 -0
  25. package/src/components/annotation/PlannotateAnnotationReport.jsx +33 -0
  26. package/src/components/annotation/useUpdateAnnotationInMainSequence.js +39 -0
  27. package/src/components/assembler/AssemblePartWidget.jsx +252 -0
  28. package/src/components/assembler/Assembler.jsx +273 -0
  29. package/src/components/assembler/AssemblerPart.jsx +99 -0
  30. package/src/components/assembler/StopIcon.jsx +34 -0
  31. package/src/components/assembler/assembler_data2.json +50 -0
  32. package/src/components/assembler/assembly_component.module.css +81 -0
  33. package/src/components/assembler/moclo.json +110 -0
  34. package/src/components/assembler/sbol_visual_glyphs/LICENSE.html +21 -0
  35. package/src/components/assembler/sbol_visual_glyphs/assembly-scar.svg +63 -0
  36. package/src/components/assembler/sbol_visual_glyphs/cds-stop.svg +85 -0
  37. package/src/components/assembler/sbol_visual_glyphs/cds.svg +60 -0
  38. package/src/components/assembler/sbol_visual_glyphs/chromosomal-locus.svg +78 -0
  39. package/src/components/assembler/sbol_visual_glyphs/engineered-region.svg +56 -0
  40. package/src/components/assembler/sbol_visual_glyphs/five-prime-sticky-restriction-site.svg +56 -0
  41. package/src/components/assembler/sbol_visual_glyphs/origin-of-replication.svg +57 -0
  42. package/src/components/assembler/sbol_visual_glyphs/primer-binding-site.svg +59 -0
  43. package/src/components/assembler/sbol_visual_glyphs/promoter.svg +60 -0
  44. package/src/components/assembler/sbol_visual_glyphs/ribosome-entry-site.svg +56 -0
  45. package/src/components/assembler/sbol_visual_glyphs/specific-recombination-site.svg +59 -0
  46. package/src/components/assembler/sbol_visual_glyphs/terminator.svg +60 -0
  47. package/src/components/assembler/sbol_visual_glyphs/three-prime-sticky-restriction-site.svg +56 -0
  48. package/src/components/assembler/sbol_visual_glyphs.js +36 -0
  49. package/src/components/assembler/useAssembler.js +71 -0
  50. package/src/components/dummy/DummyInterface.js +41 -0
  51. package/src/components/dummy/GetSequenceFileAndDatabaseIdComponent.jsx +59 -0
  52. package/src/components/eLabFTW/ELabFTWCategorySelect.cy.jsx +86 -0
  53. package/src/components/eLabFTW/ELabFTWCategorySelect.jsx +43 -0
  54. package/src/components/eLabFTW/ELabFTWFileSelect.cy.jsx +43 -0
  55. package/src/components/eLabFTW/ELabFTWFileSelect.jsx +29 -0
  56. package/src/components/eLabFTW/ELabFTWResourceSelect.cy.jsx +107 -0
  57. package/src/components/eLabFTW/ELabFTWResourceSelect.jsx +23 -0
  58. package/src/components/eLabFTW/GetPrimerComponent.cy.jsx +261 -0
  59. package/src/components/eLabFTW/GetPrimerComponent.jsx +55 -0
  60. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.cy.jsx +184 -0
  61. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.jsx +62 -0
  62. package/src/components/eLabFTW/LoadHistoryComponent.cy.jsx +235 -0
  63. package/src/components/eLabFTW/LoadHistoryComponent.jsx +51 -0
  64. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.cy.jsx +159 -0
  65. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.jsx +54 -0
  66. package/src/components/eLabFTW/SubmitToDatabaseComponent.cy.jsx +185 -0
  67. package/src/components/eLabFTW/SubmitToDatabaseComponent.jsx +51 -0
  68. package/src/components/eLabFTW/common.js +26 -0
  69. package/src/components/eLabFTW/eLabFTWInterface.js +294 -0
  70. package/src/components/eLabFTW/eLabFTWInterface.test.js +839 -0
  71. package/src/components/eLabFTW/envValues.js +7 -0
  72. package/src/components/eLabFTW/utils.js +39 -0
  73. package/src/components/form/CustomFormHelperText.jsx +10 -0
  74. package/src/components/form/EnzymeMultiSelect.cy.jsx +61 -0
  75. package/src/components/form/EnzymeMultiSelect.jsx +34 -0
  76. package/src/components/form/GetRequestMultiSelect.jsx +107 -0
  77. package/src/components/form/LabelWithTooltip.jsx +16 -0
  78. package/src/components/form/PostRequestSelect.cy.jsx +70 -0
  79. package/src/components/form/PostRequestSelect.jsx +86 -0
  80. package/src/components/form/RequestStatusWrapper.jsx +17 -0
  81. package/src/components/form/RetryAlert.jsx +20 -0
  82. package/src/components/form/ServerErrorMessage.jsx +10 -0
  83. package/src/components/form/SubmitButtonBackendAPI.jsx +15 -0
  84. package/src/components/form/SubmitToDatabaseDialog.jsx +133 -0
  85. package/src/components/form/TextFieldValidate.jsx +67 -0
  86. package/src/components/form/ValidatedTextField.jsx +33 -0
  87. package/src/components/form/intermediates_disclaimer.svg +181 -0
  88. package/src/components/navigation/ButtonWithMenu.jsx +43 -0
  89. package/src/components/navigation/CustomTab.jsx +14 -0
  90. package/src/components/navigation/FeedbackDialog.jsx +34 -0
  91. package/src/components/navigation/GithubCornerRight.jsx +29 -0
  92. package/src/components/navigation/MainAppBar.css +26 -0
  93. package/src/components/navigation/MainAppBar.jsx +205 -0
  94. package/src/components/navigation/SelectExampleDialog.jsx +69 -0
  95. package/src/components/navigation/SelectTemplateDialog.jsx +107 -0
  96. package/src/components/navigation/TabPanel.jsx +28 -0
  97. package/src/components/navigation/VersionDialog.jsx +33 -0
  98. package/src/components/primers/CreatePrimerFromSequenceForm.jsx +42 -0
  99. package/src/components/primers/DownloadPrimersButton.jsx +104 -0
  100. package/src/components/primers/PrimerForm.css +14 -0
  101. package/src/components/primers/PrimerForm.jsx +107 -0
  102. package/src/components/primers/PrimerList.css +46 -0
  103. package/src/components/primers/PrimerList.cy.jsx +95 -0
  104. package/src/components/primers/PrimerList.jsx +126 -0
  105. package/src/components/primers/PrimerTableRow.cy.jsx +57 -0
  106. package/src/components/primers/PrimerTableRow.jsx +84 -0
  107. package/src/components/primers/SelectPrimerForm.jsx +66 -0
  108. package/src/components/primers/import_primers/ImportPrimersButton.jsx +101 -0
  109. package/src/components/primers/import_primers/ImportPrimersTable.jsx +51 -0
  110. package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +107 -0
  111. package/src/components/primers/import_primers/styles.css +60 -0
  112. package/src/components/primers/primer_design/SequenceTabComponents/CollapsableLabel.jsx +31 -0
  113. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +164 -0
  114. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +37 -0
  115. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +369 -0
  116. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignEBIC.jsx +24 -0
  117. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +29 -0
  118. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +36 -0
  119. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +26 -0
  120. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +32 -0
  121. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +25 -0
  122. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +25 -0
  123. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignStepper.jsx +53 -0
  124. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +80 -0
  125. package/src/components/primers/primer_design/SequenceTabComponents/PrimerResultForm.jsx +49 -0
  126. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSettingsForm.jsx +68 -0
  127. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +84 -0
  128. package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +85 -0
  129. package/src/components/primers/primer_design/SequenceTabComponents/StepNavigation.jsx +48 -0
  130. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +216 -0
  131. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +42 -0
  132. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +61 -0
  133. package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +59 -0
  134. package/src/components/primers/primer_design/SequenceTabComponents/primerDesignMinimalValues.json +5 -0
  135. package/src/components/primers/primer_design/SequenceTabComponents/useEBICPrimerDesignSettings.js +31 -0
  136. package/src/components/primers/primer_design/SequenceTabComponents/useEnzymePrimerDesignSettings.js +57 -0
  137. package/src/components/primers/primer_design/SequenceTabComponents/useGatewayPrimerDesignSettings.js +18 -0
  138. package/src/components/primers/primer_design/SequenceTabComponents/usePrimerDesignSettings.js +31 -0
  139. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +88 -0
  140. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +65 -0
  141. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +84 -0
  142. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +74 -0
  143. package/src/components/primers/primer_design/common/NoAttPSitesError.jsx +31 -0
  144. package/src/components/primers/primer_details/PCRTable.cy.jsx +51 -0
  145. package/src/components/primers/primer_details/PCRTable.jsx +35 -0
  146. package/src/components/primers/primer_details/Primer3Figure.jsx +25 -0
  147. package/src/components/primers/primer_details/PrimerDetailsTds.jsx +39 -0
  148. package/src/components/primers/primer_details/PrimerInfoIcon.cy.jsx +137 -0
  149. package/src/components/primers/primer_details/PrimerInfoIcon.jsx +132 -0
  150. package/src/components/primers/primer_details/TableSection.jsx +17 -0
  151. package/src/components/primers/primer_details/primerDetailsFormatting.js +3 -0
  152. package/src/components/primers/primer_details/useMultiplePrimerDetails.js +29 -0
  153. package/src/components/primers/primer_details/usePCRDetails.js +47 -0
  154. package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +49 -0
  155. package/src/components/primers/primer_details/useSinglePrimerSequenceDetails.js +25 -0
  156. package/src/components/primers/primersToTabularFile.js +49 -0
  157. package/src/components/primers/primersToTabularFile.test.js +108 -0
  158. package/src/components/settings/SettingsTab.cy.jsx +267 -0
  159. package/src/components/settings/SettingsTab.jsx +170 -0
  160. package/src/components/sources/AssemblyPlanDisplayer.cy.jsx +22 -0
  161. package/src/components/sources/AssemblyPlanDisplayer.jsx +27 -0
  162. package/src/components/sources/CollectionSource.jsx +97 -0
  163. package/src/components/sources/FinishedSource.jsx +397 -0
  164. package/src/components/sources/KnownSourceErrors.jsx +50 -0
  165. package/src/components/sources/MultipleInputsSelector.jsx +63 -0
  166. package/src/components/sources/MultipleOutputsSelector.jsx +63 -0
  167. package/src/components/sources/NewSourceBox.jsx +37 -0
  168. package/src/components/sources/PCRUnitForm.jsx +102 -0
  169. package/src/components/sources/SingleInputSelector.jsx +36 -0
  170. package/src/components/sources/Source.jsx +125 -0
  171. package/src/components/sources/SourceAnnotation.jsx +44 -0
  172. package/src/components/sources/SourceAssembly.jsx +201 -0
  173. package/src/components/sources/SourceBox.css +18 -0
  174. package/src/components/sources/SourceBox.jsx +60 -0
  175. package/src/components/sources/SourceCopySequence.jsx +38 -0
  176. package/src/components/sources/SourceDatabase.jsx +28 -0
  177. package/src/components/sources/SourceFile.jsx +188 -0
  178. package/src/components/sources/SourceGenomeRegion.cy.jsx +131 -0
  179. package/src/components/sources/SourceGenomeRegion.jsx +486 -0
  180. package/src/components/sources/SourceHomologousRecombination.jsx +125 -0
  181. package/src/components/sources/SourceKnownGenomeRegion.jsx +60 -0
  182. package/src/components/sources/SourceManuallyTyped.jsx +116 -0
  183. package/src/components/sources/SourcePCRorHybridization.jsx +165 -0
  184. package/src/components/sources/SourcePolymeraseExtension.jsx +44 -0
  185. package/src/components/sources/SourceRepositoryId.jsx +409 -0
  186. package/src/components/sources/SourceRestriction.jsx +41 -0
  187. package/src/components/sources/SourceReverseComplement.jsx +33 -0
  188. package/src/components/sources/SourceTypeSelector.jsx +94 -0
  189. package/src/components/sources/SubSequenceDisplayer.jsx +70 -0
  190. package/src/components/sources/VerifyDeleteDialog.jsx +23 -0
  191. package/src/components/sources/repositoryMetadata.js +14 -0
  192. package/src/components/verification/LoadFromDatabaseButton.jsx +90 -0
  193. package/src/components/verification/SequencingFileRow.jsx +34 -0
  194. package/src/components/verification/VerificationFileDialog.cy.jsx +176 -0
  195. package/src/components/verification/VerificationFileDialog.jsx +248 -0
  196. package/src/config/defaultMainEditorProps.js +44 -0
  197. package/src/hooks/useAlerts.js +16 -0
  198. package/src/hooks/useBackendAPI.js +51 -0
  199. package/src/hooks/useBackendRoute.js +22 -0
  200. package/src/hooks/useDatabase.js +18 -0
  201. package/src/hooks/useDragAndDropFile.js +31 -0
  202. package/src/hooks/useGatewaySites.js +40 -0
  203. package/src/hooks/useHttpClient.js +12 -0
  204. package/src/hooks/useLoadDatabaseFile.js +108 -0
  205. package/src/hooks/useStoreEditor.js +101 -0
  206. package/src/hooks/useValidateState.js +43 -0
  207. package/vitest.config.js +18 -0
@@ -0,0 +1,166 @@
1
+ import React from 'react';
2
+ import { batch, useDispatch, useStore } from 'react-redux';
3
+ import useAlerts from '../hooks/useAlerts';
4
+ import useBackendRoute from '../hooks/useBackendRoute';
5
+ import useValidateState from '../hooks/useValidateState';
6
+ import { cloningActions } from '@opencloning/store/cloning';
7
+ import { mergeStates } from '@opencloning/utils/network';
8
+ import { loadFilesToSessionStorage, loadHistoryFile, updateVerificationFileNames } from '@opencloning/utils/readNwrite';
9
+ import HistoryLoadedDialog from './HistoryLoadedDialog';
10
+ import useHttpClient from '../hooks/useHttpClient';
11
+ import { getVerificationFileName } from '@opencloning/utils/readNwrite';
12
+ import { isEqual } from 'lodash-es';
13
+
14
+ const { setState: setCloningState, deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
15
+
16
+ async function processSequenceFiles(files, backendRoute, httpClient) {
17
+ const allSources = [];
18
+ const allSequences = [];
19
+ const allWarnings = [];
20
+
21
+ const processFile = async (file) => {
22
+ const fileName = file.name;
23
+ const fileContent = await file.arrayBuffer();
24
+ const newFile = new File([fileContent], fileName);
25
+
26
+ const formData = new FormData();
27
+ formData.append('file', newFile);
28
+ const config = {
29
+ headers: {
30
+ 'content-type': 'multipart/form-data',
31
+ },
32
+ };
33
+ const url = backendRoute('read_from_file');
34
+ try {
35
+ const { data: { sources, sequences }, headers } = await httpClient.post(url, formData, config);
36
+ // If there are warnings, add them to the list of warnings
37
+ const warnings = headers['x-warning'] ? [`${fileName}: ${headers['x-warning']}`] : [];
38
+ return { sources, sequences, warnings };
39
+ } catch (e) {
40
+ if (e.code === 'ERR_NETWORK') {
41
+ throw new Error('Cannot connect to backend server');
42
+ } else {
43
+ console.error(e);
44
+ throw new Error(`Could not read the file ${fileName}`);
45
+ }
46
+ }
47
+ };
48
+
49
+ const results = await Promise.all(Array.from(files).map((file) => processFile(file)));
50
+
51
+ results.forEach(({ sources, sequences, warnings }) => {
52
+ allSources.push(...sources);
53
+ allSequences.push(...sequences);
54
+ allWarnings.push(...warnings);
55
+ });
56
+
57
+ return { sources: allSources, sequences: allSequences, warnings: allWarnings };
58
+ }
59
+
60
+ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
61
+ const { addAlert } = useAlerts();
62
+ const dispatch = useDispatch();
63
+ const backendRoute = useBackendRoute();
64
+ const store = useStore();
65
+ const validateState = useValidateState();
66
+ const httpClient = useHttpClient();
67
+
68
+ const [fileLoaderFunctions, setFileLoaderFunctions] = React.useState(null);
69
+
70
+ React.useEffect(() => {
71
+ if (fileList.length === 0) {
72
+ return;
73
+ }
74
+ setFileLoaderFunctions(null);
75
+ const files = Array.from(fileList);
76
+ clearFiles();
77
+ const processFileSubmission = async () => {
78
+ const cloningState = store.getState().cloning;
79
+ const isZipFile = files.length === 1 && files[0].name.endsWith('.zip');
80
+ const isJsonFile = files.length === 1 && files[0].name.endsWith('.json');
81
+ // If the file is a zip or json file, load the history file
82
+ if (isZipFile || isJsonFile) {
83
+ let cloningStrategy;
84
+ let verificationFiles;
85
+ try {
86
+ ({ cloningStrategy, verificationFiles } = await loadHistoryFile(files[0]));
87
+ } catch (e) {
88
+ console.error(e);
89
+ addAlert({ message: e.message, severity: 'error' });
90
+ return;
91
+ }
92
+
93
+ const validatedState = await validateState(cloningStrategy);
94
+ // Update the verificationFiles names if needed
95
+ const updatedVerificationFiles = updateVerificationFileNames(verificationFiles, cloningStrategy.files, validatedState.files);
96
+
97
+ const updateState = async (newState, idShift) => {
98
+ dispatch(setCloningState(newState));
99
+ await loadFilesToSessionStorage(updatedVerificationFiles, idShift);
100
+ };
101
+
102
+ const replaceState = async () => {
103
+ await updateState(validatedState, 0);
104
+ };
105
+
106
+ const addState = async () => {
107
+ const { mergedState, idShift } = mergeStates(validatedState, cloningState);
108
+ await updateState(mergedState, idShift);
109
+ };
110
+
111
+ const errorWrapper = (fn) => async () => {
112
+ try {
113
+ await fn();
114
+ } catch (e) {
115
+ console.error(e);
116
+ addAlert({ message: e.message, severity: 'error' });
117
+ }
118
+ };
119
+ // If there are no sequences in the cloning state, load the history file
120
+ if (cloningState.sequences.length === 0) {
121
+ errorWrapper(replaceState)();
122
+ } else {
123
+ setFileLoaderFunctions({ addState: errorWrapper(addState), replaceState: errorWrapper(replaceState), clear: () => setFileLoaderFunctions(null) });
124
+ }
125
+ // If there are sequences in the cloning state, give the option to merge or replace
126
+
127
+ // You cannot drop a zip or json file and a sequence file at the same time
128
+ } else if (files.some((file) => file.name.endsWith('.json') || file.name.endsWith('.zip'))) {
129
+ addAlert({
130
+ message: 'Drop either a single JSON/zip file or multiple sequence files. Not both.',
131
+ severity: 'error',
132
+ });
133
+ // Process a bunch of sequence files
134
+ } else {
135
+ try {
136
+ const { sources, sequences, warnings } = await processSequenceFiles(files, backendRoute, httpClient);
137
+ batch(() => {
138
+ // If there is only one source and it is empty, delete it
139
+ if (cloningState.sources.length === 1 && cloningState.sources[0].type === null) {
140
+ dispatch(deleteSourceAndItsChildren(cloningState.sources[0].id));
141
+ }
142
+ for (let i = 0; i < sources.length; i += 1) {
143
+ dispatch(addSourceAndItsOutputSequence({ source: sources[i], sequence: sequences[i] }));
144
+ }
145
+ warnings.forEach((warning) => addAlert({ message: warning, severity: 'warning' }));
146
+ });
147
+ } catch (e) {
148
+ console.error(e);
149
+ addAlert({
150
+ message: e.message,
151
+ severity: 'error',
152
+ });
153
+ }
154
+ }
155
+ };
156
+ processFileSubmission();
157
+ }, [fileList]);
158
+ return (
159
+ <>
160
+ {fileLoaderFunctions && <HistoryLoadedDialog fileLoaderFunctions={fileLoaderFunctions} />}
161
+ {children}
162
+ </>
163
+ );
164
+ }
165
+
166
+ export default LoadCloningHistoryWrapper;
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import VisibilityIcon from '@mui/icons-material/Visibility';
3
+ import DownloadIcon from '@mui/icons-material/Download';
4
+ import EditIcon from '@mui/icons-material/Edit';
5
+ import CheckIcon from '@mui/icons-material/Rule';
6
+ import Tooltip from '@mui/material/Tooltip';
7
+ import { useDispatch, useSelector, useStore } from 'react-redux';
8
+ import { isEqual } from 'lodash-es';
9
+ import { cloningActions } from '@opencloning/store/cloning';
10
+ import useStoreEditor from '../hooks/useStoreEditor';
11
+ import DownloadSequenceFileDialog from './DownloadSequenceFileDialog';
12
+ import EditSequenceNameDialog from './EditSequenceNameDialog';
13
+ import VerificationFileDialog from './verification/VerificationFileDialog';
14
+ import SubmitToDatabaseDialog from './form/SubmitToDatabaseDialog';
15
+ import { getSourceDatabaseId } from '@opencloning/store/cloning_utils';
16
+ import useDatabase from '../hooks/useDatabase';
17
+
18
+ function MainSequenceCheckBox({ id }) {
19
+ const [downloadDialogOpen, setDownloadDialogOpen] = React.useState(false);
20
+ const [editNameDialogOpen, setEditNameDialogOpen] = React.useState(false);
21
+ const [verificationDialogOpen, setVerificationDialogOpen] = React.useState(false);
22
+ const [eLabDialogOpen, setELabDialogOpen] = React.useState(false);
23
+ const dispatch = useDispatch();
24
+ const database = useDatabase();
25
+ const { updateStoreEditor } = useStoreEditor();
26
+ const store = useStore();
27
+ const { setMainSequenceId, setCurrentTab } = cloningActions;
28
+ const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
29
+ const hasVerificationFiles = useSelector((state) => state.cloning.files.some((file) => file.sequence_id === id));
30
+
31
+ const databaseId = useSelector((state) => getSourceDatabaseId(state.cloning.sources, id), isEqual);
32
+ const hasDatabaseId = Boolean(databaseId);
33
+ const toggleMain = () => {
34
+ const { mainSequenceId } = store.getState().cloning;
35
+ if (mainSequenceId !== id) {
36
+ dispatch(setMainSequenceId(id));
37
+ updateStoreEditor('mainEditor', id);
38
+ }
39
+ dispatch(setCurrentTab(3));
40
+ // TODO: ideally this should be done with a ref
41
+ document.getElementById('opencloning-app-tabs')?.scrollIntoView();
42
+ };
43
+ const tooltipText = <div className="tooltip-text">See sequence in main editor</div>;
44
+
45
+ const DatabaseIcon = hasDatabaseId ? database?.DatabaseIcon : database?.SubmitIcon;
46
+
47
+ return (
48
+ <div className="node-corner">
49
+ {downloadDialogOpen && <DownloadSequenceFileDialog {...{ id, dialogOpen: downloadDialogOpen, setDialogOpen: setDownloadDialogOpen }} />}
50
+ {editNameDialogOpen && <EditSequenceNameDialog {...{ id, dialogOpen: editNameDialogOpen, setDialogOpen: setEditNameDialogOpen }} />}
51
+ {verificationDialogOpen && <VerificationFileDialog {...{ id, dialogOpen: verificationDialogOpen, setDialogOpen: setVerificationDialogOpen }} />}
52
+ {eLabDialogOpen && <SubmitToDatabaseDialog {...{ id, dialogOpen: eLabDialogOpen, setDialogOpen: setELabDialogOpen, resourceType: 'sequence' }} />}
53
+ {database && (
54
+ <Tooltip title={hasDatabaseId ? `Stored in ${database.name}` : `Submit to ${database.name}`} arrow placement="top">
55
+ <DatabaseIcon
56
+ onClick={() => (hasDatabaseId ? window.open(database.getSequenceLink(databaseId), '_blank') : setELabDialogOpen(true))}
57
+ type="button"
58
+ className="node-corner-icon"
59
+ sx={{
60
+ color: hasDatabaseId ? 'success.main' : 'gray',
61
+ cursor: 'pointer',
62
+ }}
63
+ />
64
+ </Tooltip>
65
+ )}
66
+ <Tooltip title="Verification files" arrow placement="top">
67
+ <CheckIcon onClick={() => setVerificationDialogOpen(true)} type="button" className="node-corner-icon" sx={{ color: hasVerificationFiles ? '#2e7d32' : 'gray', cursor: 'pointer', '&:hover': { filter: 'brightness(70%)' } }} />
68
+ </Tooltip>
69
+ <Tooltip title="Change name" arrow placement="top">
70
+ <EditIcon onClick={() => setEditNameDialogOpen(true)} type="button" className="node-corner-icon" sx={{ color: 'gray', cursor: 'pointer', '&:hover': { filter: 'brightness(70%)' } }} />
71
+ </Tooltip>
72
+ <Tooltip title={tooltipText} arrow placement="top">
73
+ <VisibilityIcon onClick={toggleMain} type="button" className="node-corner-icon" sx={{ cursor: 'pointer', color: (id === mainSequenceId) ? '#2e7d32' : 'gray', '&:hover': { filter: 'brightness(70%)' } }} />
74
+ </Tooltip>
75
+ <Tooltip title="Download sequence" arrow placement="top">
76
+ <DownloadIcon onClick={() => setDownloadDialogOpen(true)} type="button" className="node-corner-icon" sx={{ color: 'gray', cursor: 'pointer', '&:hover': { filter: 'brightness(70%)' } }} />
77
+ </Tooltip>
78
+
79
+ </div>
80
+ );
81
+ }
82
+
83
+ export default MainSequenceCheckBox;
@@ -0,0 +1,165 @@
1
+ import React from 'react';
2
+ import { Editor, updateEditor } from '@teselagen/ove';
3
+ import { useDispatch, useStore } from 'react-redux';
4
+ import { getReverseComplementSequenceString, getSequenceDataBetweenRange } from '@teselagen/sequence-utils';
5
+ import defaultMainEditorProps from '../config/defaultMainEditorProps';
6
+ import { cloningActions } from '@opencloning/store/cloning';
7
+ import useAlerts from '../hooks/useAlerts';
8
+ import { Alert, Button } from '@mui/material';
9
+ import { useSelector } from 'react-redux';
10
+ import useUpdateAnnotationInMainSequence from './annotation/useUpdateAnnotationInMainSequence';
11
+ import useStoreEditor from '../hooks/useStoreEditor';
12
+
13
+ const { setMainSequenceSelection, addPrimer } = cloningActions;
14
+
15
+ function regionRightClickedOverride(items, { annotation }, props) {
16
+ const items2keep = items.filter((i) => i.text === 'Copy');
17
+ return [
18
+ ...items2keep,
19
+ {
20
+ text: 'Create',
21
+ submenu: [
22
+ "newFeature",
23
+ "newPrimer",
24
+ ],
25
+ },
26
+ ...(props.sequenceData.circular === true ? [
27
+ "--",
28
+ "selectInverse",
29
+ "--",
30
+ ] : []),
31
+ ];
32
+ }
33
+ function primerRightClickedOverride(items, { annotation }, props) {
34
+ return [
35
+ ...regionRightClickedOverride(items, { annotation }, props),
36
+ "--",
37
+ {
38
+ text: 'Delete Primer annotation',
39
+ cmd: 'deletePrimer',
40
+ }
41
+ ];
42
+ }
43
+
44
+ function featureRightClickedOverride(items, { annotation }, props) {
45
+ return [
46
+ ...regionRightClickedOverride(items, { annotation }, props),
47
+ "--",
48
+ "editFeature",
49
+ "deleteFeature",
50
+ "showRemoveDuplicatesDialogFeatures",
51
+ "--",
52
+ "toggleCdsFeatureTranslations",
53
+ "viewFeatureProperties",
54
+ ];
55
+ }
56
+
57
+
58
+ function MainSequenceEditor() {
59
+ const dispatch = useDispatch();
60
+ const { addAlert } = useAlerts();
61
+ const { updateStoreEditor } = useStoreEditor();
62
+ const updateAnnotationInMainSequence = useUpdateAnnotationInMainSequence();
63
+
64
+ const annotationChanged = useSelector(
65
+ (state) => {
66
+ const history = state.VectorEditor.mainEditor?.sequenceDataHistory;
67
+ if (!history) return false;
68
+ return state.cloning.mainSequenceId && Object.keys(history).length > 0 && history.future.length === 0;
69
+ }
70
+ );
71
+
72
+ const store = useStore();
73
+ const editorName = 'mainEditor';
74
+ const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
75
+ const topDivRef = React.useRef(null);
76
+
77
+ React.useEffect(() => {
78
+ if (annotationChanged) {
79
+ setTimeout(() => {
80
+ topDivRef.current.scrollIntoView({ behavior: 'smooth' });
81
+ }, 250);
82
+ }
83
+ }, [annotationChanged]);
84
+
85
+
86
+ const onAnnotationCancel = () => {
87
+ const currentSequenceData = store.getState().VectorEditor.mainEditor.sequenceData;
88
+ updateStoreEditor('mainEditor', mainSequenceId, { sequenceData: currentSequenceData });
89
+ };
90
+
91
+ const beforeAnnotationCreate = ({ annotationTypePlural, annotation, props, isEdit }) => {
92
+
93
+ if (annotationTypePlural === 'primers') {
94
+ const existingPrimerNames = store.getState().cloning.primers.map((p) => p.name);
95
+ if (existingPrimerNames.includes(annotation.name)) {
96
+ addAlert({
97
+ message: `A primer with name "${annotation.name}" already exists`,
98
+ severity: 'error',
99
+ });
100
+ return false;
101
+ }
102
+ let { sequence } = getSequenceDataBetweenRange(props.sequenceData, annotation);
103
+ if (annotation.strand === -1) {
104
+ sequence = getReverseComplementSequenceString(sequence);
105
+ }
106
+ dispatch(addPrimer({
107
+ name: annotation.name,
108
+ sequence: sequence,
109
+ }));
110
+ addAlert({
111
+ message: `Primer "${annotation.name}" created`,
112
+ severity: 'success',
113
+ });
114
+ } else if (annotationTypePlural !== 'features') {
115
+ return false;
116
+ }
117
+ };
118
+
119
+ const extraProp = {
120
+ beforeAnnotationCreate,
121
+ onSelectionOrCaretChanged: (a) => dispatch(setMainSequenceSelection(a)),
122
+ selectionLayer: {},
123
+ sequenceData: {},
124
+ rightClickOverrides: {
125
+ selectionLayerRightClicked: regionRightClickedOverride,
126
+ primerRightClicked: primerRightClickedOverride,
127
+ translationRightClicked: regionRightClickedOverride,
128
+ searchLayerRightClicked: regionRightClickedOverride,
129
+ featureRightClicked: featureRightClickedOverride,
130
+ partRightClicked: {},
131
+ orfRightClicked: {},
132
+ backgroundRightClicked: {},
133
+ },
134
+ };
135
+
136
+ React.useEffect(() => {
137
+ updateEditor(store, editorName, { ...defaultMainEditorProps, ...extraProp });
138
+ }, []);
139
+
140
+ return (
141
+ <div style={{ textAlign: 'left' }} ref={topDivRef}>
142
+ {annotationChanged &&
143
+ <Alert
144
+ style={{maxWidth: '500px', margin: '10px auto'}}
145
+ severity="info"
146
+ data-testid="annotation-changed-alert"
147
+ action={
148
+ <>
149
+ <Button color="primary" onClick={updateAnnotationInMainSequence}>
150
+ Save
151
+ </Button>
152
+ <Button color="secondary" onClick={onAnnotationCancel}>
153
+ Cancel
154
+ </Button>
155
+ </>
156
+ }
157
+ >
158
+ <strong>Annotation Changed</strong>
159
+ </Alert>
160
+ }
161
+ <Editor {...{ editorName, ...defaultMainEditorProps, ...extraProp, height: '800' }} />
162
+ </div>
163
+ );
164
+ }
165
+ export default React.memo(MainSequenceEditor);
@@ -0,0 +1,159 @@
1
+ import React, { useRef } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { Box, Tooltip } from '@mui/material';
4
+ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
5
+ import VisibilityIcon from '@mui/icons-material/Visibility';
6
+ import AddCircleIcon from '@mui/icons-material/AddCircle';
7
+ import { isEqual } from 'lodash-es';
8
+ import Source from './sources/Source';
9
+ import './NetworkTree.css';
10
+ import SequenceEditor from './SequenceEditor';
11
+ import FinishedSource from './sources/FinishedSource';
12
+ import MainSequenceCheckBox from './MainSequenceCheckBox';
13
+ import TemplateSequence from './TemplateSequence';
14
+ import { getSourceDatabaseId, isSourceATemplate } from '@opencloning/store/cloning_utils';
15
+ import { cloningActions } from '@opencloning/store/cloning';
16
+ import SourceBox from './sources/SourceBox';
17
+ import { getSortedSourceIds } from '@opencloning/utils/network';
18
+ import useDatabase from '../hooks/useDatabase';
19
+
20
+ const { addToSourcesWithHiddenAncestors, removeFromSourcesWithHiddenAncestors, addSequenceInBetween } = cloningActions;
21
+
22
+ const SequenceContent = React.memo(({ sequenceId, sequenceIsTemplate }) => {
23
+ const database = useDatabase();
24
+ const isSavedToDatabase = database && useSelector((state) => Boolean(getSourceDatabaseId(state.cloning.sources, sequenceId)));
25
+ return (
26
+ <span className="tf-nc" style={{ borderColor: isSavedToDatabase ? 'green' : 'default' }}>
27
+ <span className="node-text">
28
+ {sequenceIsTemplate ? (
29
+ <TemplateSequence sequenceId={sequenceId} />
30
+ ) : (
31
+ <>
32
+ <SequenceEditor {...{ sequenceId }} />
33
+ <MainSequenceCheckBox {...{ id: sequenceId }} />
34
+ </>
35
+ )}
36
+ <div className="corner-id" style={{ color: isSavedToDatabase ? 'green' : 'default' }}>{sequenceId}</div>
37
+ </span>
38
+ </span>
39
+ );
40
+ });
41
+
42
+ function SequenceWrapper({ children, sequenceId, sequenceIsTemplate }) {
43
+ if (sequenceId === null) {
44
+ return children;
45
+ }
46
+ return (
47
+ <li key={sequenceId} id={`sequence-${sequenceId}`} className="sequence-node">
48
+ <SequenceContent {...{ sequenceId, sequenceIsTemplate }} />
49
+ <ul>
50
+ {children}
51
+ </ul>
52
+ </li>
53
+ );
54
+ }
55
+
56
+ // A component that renders the ancestry tree
57
+ function NetWorkNode({ sourceId }) {
58
+ const tooltipRef = useRef(null);
59
+ const info = useSelector((state) => {
60
+ const s = state.cloning.sources.find((source) => source.id === sourceId);
61
+ const sequenceId = state.cloning.sequences.map((sequence) => sequence.id).includes(s.id) ? s.id : null;
62
+ return {
63
+ sequenceId,
64
+ sourceInput: s.input,
65
+ hasDatabaseId: Boolean(getSourceDatabaseId(state.cloning.sources, s.id)),
66
+ sequenceIsTemplate: sequenceId && state.cloning.sequences.find((sequence) => sequence.id === sequenceId).type === 'TemplateSequence',
67
+ sourceIsTemplate: isSourceATemplate(state.cloning, sourceId),
68
+ };
69
+ }, isEqual);
70
+ const { sequenceId, sourceInput, hasDatabaseId, sequenceIsTemplate, sourceIsTemplate } = info;
71
+ const ancestorsHidden = useSelector((state) => state.cloning.sourcesWithHiddenAncestors.includes(sourceId));
72
+ const parentSourceIds = useSelector((state) => {
73
+ const parentSources = state.cloning.sources.filter((source) => sourceInput.some(({sequence}) => sequence === source.id));
74
+ return getSortedSourceIds(parentSources, state.cloning.sources);
75
+ }, isEqual);
76
+
77
+ const dispatch = useDispatch();
78
+ const database = useDatabase();
79
+
80
+ const onVisibilityClick = React.useCallback(() => {
81
+ if (ancestorsHidden) {
82
+ dispatch(removeFromSourcesWithHiddenAncestors(sourceId));
83
+ // Give it a bit of time to render the ancestors
84
+ setTimeout(() => {
85
+ // If it has children sequence align to the children
86
+ if (sequenceId) {
87
+ document.getElementById(`sequence-${sequenceId}`)?.scrollIntoView({ alignToTop: false, block: 'end' });
88
+ } else {
89
+ document.getElementById(`source-${sourceId}`)?.scrollIntoView({ alignToTop: false, block: 'end' });
90
+ }
91
+ }, 100);
92
+ } else {
93
+ dispatch(addToSourcesWithHiddenAncestors(sourceId));
94
+ }
95
+
96
+ if (tooltipRef.current) {
97
+ tooltipRef.current.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
98
+ }
99
+ }, [ancestorsHidden, sourceId, sequenceId]);
100
+
101
+ const Icon = ancestorsHidden ? VisibilityIcon : VisibilityOffIcon;
102
+ const visibilityIconToolTip = ancestorsHidden ? 'Show ancestors' : 'Hide ancestors';
103
+ const isSavedToDatabase = database && hasDatabaseId;
104
+
105
+ return (
106
+ <SequenceWrapper {...{ sequenceId, sequenceIsTemplate }}>
107
+ <li id={`source-${sourceId}`} className={`source-node ${ancestorsHidden ? 'hidden-ancestors' : ''}`}>
108
+ <Box component="span" className="tf-nc" style={{ borderColor: isSavedToDatabase ? 'green' : 'default' }}>
109
+ <span className="node-text">
110
+ <SourceBox {...{ sourceId }}>
111
+ {(sequenceId !== null && !sourceIsTemplate) ? (
112
+ <FinishedSource {...{ sourceId }} />
113
+ ) : (
114
+ <Source {...{ sourceId }} />
115
+ )}
116
+ </SourceBox>
117
+ <div className="corner-id" style={{ color: isSavedToDatabase ? 'green' : 'default' }}>
118
+ {sourceId}
119
+ </div>
120
+ { (!sourceIsTemplate && sourceInput.length > 0 && sequenceId) && (
121
+ <div className="before-node before-node-visibility">
122
+ <Tooltip
123
+ arrow
124
+ title={visibilityIconToolTip}
125
+ placement="left"
126
+ >
127
+ <div ref={tooltipRef}>
128
+ <Icon onClick={onVisibilityClick} style={{ color: 'grey' }} />
129
+ </div>
130
+ </Tooltip>
131
+ </div>
132
+ )}
133
+ { (sourceIsTemplate && sourceInput.length > 0)
134
+ && (
135
+ <div className="before-node before-node-sequence-in-between">
136
+ <Tooltip arrow title="Add sequence in between" placement={sourceInput.length > 1 ? 'top' : 'left'}>
137
+ <div>
138
+ <AddCircleIcon onClick={() => { dispatch(addSequenceInBetween(sourceId)); }} color="success" />
139
+ </div>
140
+ </Tooltip>
141
+ </div>
142
+ )}
143
+ </span>
144
+ </Box>
145
+ {parentSourceIds.length > 0 && (
146
+ <ul className={ancestorsHidden ? 'hidden-ancestors' : ''}>
147
+ {parentSourceIds.map((id) => (
148
+ <MemoizedNetWorkNode sourceId={id} key={`node-${id}`} />
149
+ ))}
150
+ </ul>
151
+ )}
152
+ </li>
153
+ </SequenceWrapper>
154
+ );
155
+ }
156
+
157
+ const MemoizedNetWorkNode = React.memo(NetWorkNode);
158
+
159
+ export default React.memo(NetWorkNode);