@opencloning/ui 1.0.2-test.0 → 1.1.0-test.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.1.0-test.2
4
+
5
+ ### Minor Changes
6
+
7
+ - d46f09d: Handle version display with scripts
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [d46f09d]
12
+ - @opencloning/store@1.1.0-test.2
13
+ - @opencloning/utils@1.1.0-test.2
14
+
15
+ ## 1.1.0-test.1
16
+
17
+ ### Minor Changes
18
+
19
+ - 02dbc55: Switch to using provider for configuration rather than state
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [02dbc55]
24
+ - @opencloning/store@1.1.0-test.1
25
+ - @opencloning/utils@1.1.0-test.1
26
+
3
27
  ## 1.0.2-test.0
4
28
 
5
29
  ### Patch Changes
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.0.2-test.0",
3
+ "version": "1.1.0-test.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
+ "scripts": {
7
+ "prepack": "node scripts/inject-version.js",
8
+ "postpack": "node scripts/reset-version.js"
9
+ },
6
10
  "exports": {
7
11
  ".": "./src/index.js",
8
- "./components": "./src/components/index.js"
12
+ "./components": "./src/components/index.js",
13
+ "./providers/ConfigProvider": "./src/providers/ConfigProvider.jsx",
14
+ "./hooks/useConfig": "./src/hooks/useConfig.js"
9
15
  },
10
16
  "repository": {
11
17
  "type": "git",
@@ -17,8 +23,8 @@
17
23
  "@emotion/styled": "^11.14.0",
18
24
  "@mui/icons-material": "^5.15.17",
19
25
  "@mui/material": "^5.15.17",
20
- "@opencloning/store": "1.0.2-test.0",
21
- "@opencloning/utils": "1.0.2-test.0",
26
+ "@opencloning/store": "1.1.0-test.2",
27
+ "@opencloning/utils": "1.1.0-test.2",
22
28
  "@teselagen/bio-parsers": "^0.4.32",
23
29
  "@teselagen/ove": "^0.8.18",
24
30
  "@teselagen/range-utils": "^0.3.13",
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
10
+ const file = join(__dirname, '..', 'src', 'version.js');
11
+
12
+ const content = `// Version placeholder - replaced at publish time via prepack script
13
+ export const version = "${pkg.version}";
14
+ `;
15
+
16
+ writeFileSync(file, content, 'utf-8');
17
+ console.log(`✓ Injected version ${pkg.version}`);
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const file = join(__dirname, '..', 'src', 'version.js');
10
+
11
+ const content = `// Version placeholder - replaced at publish time via prepack script
12
+ export const version = "__VERSION__";
13
+ `;
14
+
15
+ writeFileSync(file, content, 'utf-8');
16
+ console.log('✓ Reset version placeholder');
@@ -4,6 +4,7 @@ import { useDispatch } from 'react-redux';
4
4
  import useBackendRoute from '../hooks/useBackendRoute';
5
5
  import useHttpClient from '../hooks/useHttpClient';
6
6
  import { cloningActions } from '@opencloning/store/cloning';
7
+ import { version } from '../index';
7
8
 
8
9
  const { updateAppInfo } = cloningActions;
9
10
 
@@ -16,7 +17,7 @@ function ExternalServicesStatusCheck() {
16
17
  const backendRoute = useBackendRoute();
17
18
  const httpClient = useHttpClient();
18
19
  React.useEffect(() => {
19
- dispatch(updateAppInfo({ frontendVersion: __APP_VERSION__ }));
20
+ dispatch(updateAppInfo({ frontendVersion: version }));
20
21
  setLoading(true);
21
22
  const checkServices = async () => {
22
23
  const services = [
@@ -13,6 +13,7 @@ import CloningHistory from './CloningHistory';
13
13
  import SequenceTab from './SequenceTab';
14
14
  import AppAlerts from './AppAlerts';
15
15
  import Assembler from './assembler/Assembler';
16
+ import { useConfig } from '../hooks/useConfig';
16
17
 
17
18
  const { setCurrentTab } = cloningActions;
18
19
 
@@ -21,8 +22,7 @@ function OpenCloning() {
21
22
  const currentTab = useSelector((state) => state.cloning.currentTab);
22
23
  const tabPanelsRef = useRef(null);
23
24
  const [smallDevice, setSmallDevice] = useState(window.innerWidth < 600);
24
- const hasAppBar = useSelector((state) => state.cloning.config.showAppBar, isEqual);
25
- const enableAssembler = useSelector((state) => state.cloning.config.enableAssembler);
25
+ const { showAppBar: hasAppBar, enableAssembler } = useConfig();
26
26
 
27
27
  React.useEffect(() => {
28
28
  const handleResize = () => {
@@ -1,17 +1,20 @@
1
1
  import React from 'react';
2
2
  import EnzymeMultiSelect from './EnzymeMultiSelect';
3
- import store from '@opencloning/store';
4
- import { cloningActions } from '@opencloning/store/cloning';
3
+ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
4
+
5
+ const config = {
6
+ backendUrl: 'http://127.0.0.1:8000',
7
+ };
5
8
 
6
- const { setConfig } = cloningActions;
7
9
  describe('<EnzymeMultiSelect />', () => {
8
- beforeEach(() => {
9
- store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
10
- });
11
10
  it('can add and remove enzymes, sets enzymes', () => {
12
11
  // see: https://on.cypress.io/mounting-react
13
12
  const setEnzymesSpy = cy.spy().as('setEnzymesSpy');
14
- cy.mount(<EnzymeMultiSelect setEnzymes={setEnzymesSpy} />);
13
+ cy.mount(
14
+ <ConfigProvider config={config}>
15
+ <EnzymeMultiSelect setEnzymes={setEnzymesSpy} />
16
+ </ConfigProvider>
17
+ );
15
18
  cy.get('.MuiInputBase-root').click();
16
19
  // All enzymes shown
17
20
  cy.get('div[role="presentation"]', { timeout: 20000 }).contains('AanI');
@@ -46,7 +49,11 @@ describe('<EnzymeMultiSelect />', () => {
46
49
  statusCode: 500,
47
50
  body: 'Server down',
48
51
  });
49
- cy.mount(<EnzymeMultiSelect setEnzymes={() => {}} />);
52
+ cy.mount(
53
+ <ConfigProvider config={config}>
54
+ <EnzymeMultiSelect setEnzymes={() => {}} />
55
+ </ConfigProvider>
56
+ );
50
57
  cy.get('.MuiAlert-message').contains('Could not retrieve enzymes from server');
51
58
  });
52
59
  it('shows loading message', () => {
@@ -54,7 +61,11 @@ describe('<EnzymeMultiSelect />', () => {
54
61
  delayMs: 1000,
55
62
  body: { enzyme_names: ['EcoRI', 'SalI'] },
56
63
  });
57
- cy.mount(<EnzymeMultiSelect setEnzymes={() => {}} />);
64
+ cy.mount(
65
+ <ConfigProvider config={config}>
66
+ <EnzymeMultiSelect setEnzymes={() => {}} />
67
+ </ConfigProvider>
68
+ );
58
69
  cy.get('.MuiCircularProgress-svg');
59
70
  cy.contains('retrieving enzymes...').should('exist');
60
71
  });
@@ -3,93 +3,101 @@ import PrimerList from './PrimerList';
3
3
  import store from '@opencloning/store';
4
4
  import { cloningActions } from '@opencloning/store/cloning';
5
5
  import { Provider } from 'react-redux';
6
+ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
6
7
 
7
- const { setConfig, setPrimers, setGlobalPrimerSettings } = cloningActions;
8
+ const { setPrimers, setGlobalPrimerSettings } = cloningActions;
9
+
10
+ const config = {
11
+ backendUrl: 'http://127.0.0.1:8000',
12
+ };
8
13
 
9
14
  const mockReply = {
10
- statusCode: 200, body: {
11
- melting_temperature: 60, gc_content: .5, homodimer: {
12
- melting_temperature: 0,
13
- deltaG: 0,
14
- figure: "dummy_figure"
15
- },
16
- hairpin: {
17
- melting_temperature: 0,
18
- deltaG: 0,
19
- figure: "dummy_figure"
20
- },
21
- }
15
+ statusCode: 200, body: {
16
+ melting_temperature: 60, gc_content: .5, homodimer: {
17
+ melting_temperature: 0,
18
+ deltaG: 0,
19
+ figure: "dummy_figure"
20
+ },
21
+ hairpin: {
22
+ melting_temperature: 0,
23
+ deltaG: 0,
24
+ figure: "dummy_figure"
25
+ },
26
+ }
22
27
  }
23
28
 
24
29
  describe('PrimerList', () => {
25
- beforeEach(() => {
26
- store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
27
- });
28
- it('displays the right information', () => {
29
- store.dispatch(setPrimers([
30
- { id: 1, name: 'P1', sequence: 'TCATTAAAGTTAACG' },
31
- ]));
30
+ it('displays the right information', () => {
31
+ store.dispatch(setPrimers([
32
+ { id: 1, name: 'P1', sequence: 'TCATTAAAGTTAACG' },
33
+ ]));
32
34
 
33
- cy.mount(
34
- <Provider store={store}>
35
- <PrimerList />
36
- </Provider>
37
- );
38
- cy.get('td.name').contains('P1');
39
- cy.get('td.length').contains('15');
40
- cy.get('td.gc-content').contains('27');
41
- cy.get('td.melting-temperature').contains('37.5');
42
- cy.get('td.sequence').contains('TCATTAAAGTTAACG');
43
- });
44
-
45
- it('caches primer details across re-renders and re-renders on global settings change', () => {
46
- let calls = 0;
47
- store.dispatch(setPrimers([
48
- { id: 1, name: 'P1', sequence: 'AAA' },
49
- ]));
50
- cy.intercept('POST', 'http://127.0.0.1:8000/primer_details*', (req) => {
51
- calls += 1;
52
- const respReply = calls === 1 ? mockReply : {
53
- statusCode: 200, body: {
54
- ...mockReply.body,
55
- melting_temperature: calls === 1 ? 60 : 70,
56
- }
57
- }
58
- expect(req.body).to.deep.equal({
59
- sequence: 'AAA',
60
- settings: {
61
- primer_dna_conc: calls === 1 ? 50 : 100,
62
- primer_salt_monovalent: 50,
63
- primer_salt_divalent: 1.5,
64
- },
65
- });
66
- req.reply(respReply);
67
- }).as('primerDetails');
35
+ cy.mount(
36
+ <Provider store={store}>
37
+ <ConfigProvider config={config}>
38
+ <PrimerList />
39
+ </ConfigProvider>
40
+ </Provider>
41
+ );
42
+ cy.get('td.name').contains('P1');
43
+ cy.get('td.length').contains('15');
44
+ cy.get('td.gc-content').contains('27');
45
+ cy.get('td.melting-temperature').contains('37.5');
46
+ cy.get('td.sequence').contains('TCATTAAAGTTAACG');
47
+ });
68
48
 
69
- // First mount triggers two network calls (one per unique primer sequence)
70
- cy.mount(
71
- <Provider store={store}>
72
- <PrimerList />
73
- </Provider>)
74
- cy.contains('Loading...').should('not.exist');
75
- cy.wait('@primerDetails');
76
- cy.then(() => {
77
- expect(calls).to.equal(1);
78
- cy.mount(
79
- <Provider store={store}>
80
- <PrimerList />
81
- </Provider>)
82
- cy.then(() => {
83
- expect(calls).to.equal(1);
84
- });
85
- store.dispatch(setGlobalPrimerSettings({ primer_dna_conc: 100 }))
86
- cy.wait('@primerDetails');
87
- cy.then(() => {
88
- expect(calls).to.equal(2);
89
- cy.get('td.melting-temperature').contains('70');
90
- });
49
+ it('caches primer details across re-renders and re-renders on global settings change', () => {
50
+ let calls = 0;
51
+ store.dispatch(setPrimers([
52
+ { id: 1, name: 'P1', sequence: 'AAA' },
53
+ ]));
54
+ cy.intercept('POST', 'http://127.0.0.1:8000/primer_details*', (req) => {
55
+ calls += 1;
56
+ const respReply = calls === 1 ? mockReply : {
57
+ statusCode: 200, body: {
58
+ ...mockReply.body,
59
+ melting_temperature: calls === 1 ? 60 : 70,
60
+ }
61
+ }
62
+ expect(req.body).to.deep.equal({
63
+ sequence: 'AAA',
64
+ settings: {
65
+ primer_dna_conc: calls === 1 ? 50 : 100,
66
+ primer_salt_monovalent: 50,
67
+ primer_salt_divalent: 1.5,
68
+ },
69
+ });
70
+ req.reply(respReply);
71
+ }).as('primerDetails');
91
72
 
92
- });
73
+ // First mount triggers two network calls (one per unique primer sequence)
74
+ cy.mount(
75
+ <Provider store={store}>
76
+ <ConfigProvider config={config}>
77
+ <PrimerList />
78
+ </ConfigProvider>
79
+ </Provider>)
80
+ cy.contains('Loading...').should('not.exist');
81
+ cy.wait('@primerDetails');
82
+ cy.then(() => {
83
+ expect(calls).to.equal(1);
84
+ cy.mount(
85
+ <Provider store={store}>
86
+ <ConfigProvider config={config}>
87
+ <PrimerList />
88
+ </ConfigProvider>
89
+ </Provider>)
90
+ cy.then(() => {
91
+ expect(calls).to.equal(1);
92
+ });
93
+ store.dispatch(setGlobalPrimerSettings({ primer_dna_conc: 100 }))
94
+ cy.wait('@primerDetails');
95
+ cy.then(() => {
96
+ expect(calls).to.equal(2);
97
+ cy.get('td.melting-temperature').contains('70');
98
+ });
93
99
 
94
100
  });
101
+
102
+ });
95
103
  });
@@ -14,7 +14,6 @@ import SourceAnnotation from './SourceAnnotation';
14
14
  import SourceDatabase from './SourceDatabase';
15
15
  import SourcePolymeraseExtension from './SourcePolymeraseExtension';
16
16
  import CollectionSource from './CollectionSource';
17
- import KnownSourceErrors from './KnownSourceErrors';
18
17
  import useBackendAPI from '../../hooks/useBackendAPI';
19
18
  import MultipleOutputsSelector from './MultipleOutputsSelector';
20
19
  import { cloningActions } from '@opencloning/store/cloning';
@@ -31,7 +30,6 @@ function Source({ sourceId }) {
31
30
  const { type: sourceType } = source;
32
31
  let specificSource = null;
33
32
  const templateOnlySources = ['CollectionSource', 'KnownGenomeCoordinatesSource'];
34
- const knownErrors = useSelector((state) => state.cloning.knownErrors, isEqual);
35
33
  const { requestStatus, sendPostRequest, sources, sequences } = useBackendAPI();
36
34
  const { addSequenceAndUpdateItsSource, updateSequenceAndItsSource } = cloningActions;
37
35
  const [chosenFragment, setChosenFragment] = React.useState(null);
@@ -115,7 +113,6 @@ function Source({ sourceId }) {
115
113
  return (
116
114
  <>
117
115
  {!templateOnlySources.includes(sourceType) && (<SourceTypeSelector {...{ source }} />)}
118
- {sourceType && knownErrors[sourceType] && <KnownSourceErrors errors={knownErrors[sourceType]} />}
119
116
  {specificSource}
120
117
  {sources.length > 1 && (<MultipleOutputsSelector {...{ sources, sequences, sourceId, onFragmentChosen: setChosenFragment }} />)}
121
118
  </>
@@ -7,6 +7,7 @@ import Select from '@mui/material/Select';
7
7
  import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
8
8
  import { cloningActions } from '@opencloning/store/cloning';
9
9
  import useDatabase from '../../hooks/useDatabase';
10
+ import { useConfig } from '../../hooks/useConfig';
10
11
 
11
12
  const { replaceSource } = cloningActions;
12
13
 
@@ -15,8 +16,7 @@ function SourceTypeSelector({ source }) {
15
16
  const dispatch = useDispatch();
16
17
  const database = useDatabase();
17
18
  const sourceIsPrimerDesign = useSelector((state) => Boolean(state.cloning.sequences.find((e) => e.id === source.id)?.primer_design));
18
- const noExternalRequests = useSelector((state) => state.cloning.config.noExternalRequests);
19
- const enablePlannotate = useSelector((state) => state.cloning.config.enablePlannotate);
19
+ const { noExternalRequests, enablePlannotate } = useConfig();
20
20
 
21
21
  const onChange = (event) => {
22
22
  // Clear the source other than these fields
@@ -4,8 +4,14 @@ import store from '@opencloning/store';
4
4
  import { cloningActions } from '@opencloning/store/cloning';
5
5
  import { loadDataAndMount } from '../../../../../cypress/e2e/common_funcions_store';
6
6
  import { getVerificationFileName } from '@opencloning/utils/readNwrite';
7
+ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
8
+ import { Provider } from 'react-redux';
7
9
 
8
- const { setFiles, setConfig } = cloningActions;
10
+ const { setFiles } = cloningActions;
11
+
12
+ const config = {
13
+ backendUrl: 'http://127.0.0.1:8000',
14
+ };
9
15
 
10
16
  const dummyFiles = [
11
17
  { file_name: 'file1.txt', sequence_id: 1, file_type: 'Sequencing file' },
@@ -22,7 +28,13 @@ describe('<VerificationFileDialog />', () => {
22
28
  it('renders and calls setDialogOpen with false when clicking close button', () => {
23
29
  // see: https://on.cypress.io/mounting-react
24
30
  const setDialogOpenSpy = cy.spy().as('setDialogOpenSpy');
25
- cy.mount(<VerificationFileDialog id={1} dialogOpen setDialogOpen={setDialogOpenSpy} />);
31
+ cy.mount(
32
+ <Provider store={store}>
33
+ <ConfigProvider config={config}>
34
+ <VerificationFileDialog id={1} dialogOpen setDialogOpen={setDialogOpenSpy} />
35
+ </ConfigProvider>
36
+ </Provider>
37
+ );
26
38
 
27
39
  // Click close button
28
40
  cy.get('button').contains('Close').click();
@@ -41,7 +53,13 @@ describe('<VerificationFileDialog />', () => {
41
53
  dummyFiles.forEach((file) => {
42
54
  sessionStorage.setItem(getVerificationFileName(file), base64str);
43
55
  });
44
- cy.mount(<VerificationFileDialog id={1} dialogOpen setDialogOpen={() => {}} />);
56
+ cy.mount(
57
+ <Provider store={store}>
58
+ <ConfigProvider config={config}>
59
+ <VerificationFileDialog id={1} dialogOpen setDialogOpen={() => {}} />
60
+ </ConfigProvider>
61
+ </Provider>
62
+ );
45
63
  // Even though there are two files with the same name, only one should be displayed
46
64
  cy.get('table td').filter(':contains("file1.txt")').should('have.length', 1);
47
65
  cy.get('table').contains('file2.txt');
@@ -64,13 +82,17 @@ describe('<VerificationFileDialog />', () => {
64
82
  });
65
83
 
66
84
  it('can submit files and aligns them', () => {
67
- store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
68
-
69
85
  loadDataAndMount(
70
86
  'cypress/test_files/sequencing/cloning_strategy_linear.json',
71
87
  store,
72
88
  () => {
73
- cy.mount(<VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />);
89
+ cy.mount(
90
+ <Provider store={store}>
91
+ <ConfigProvider config={config}>
92
+ <VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />
93
+ </ConfigProvider>
94
+ </Provider>
95
+ );
74
96
  },
75
97
  ).then(() => {
76
98
  cy.get('button').contains('Submit files').click();
@@ -118,12 +140,17 @@ describe('<VerificationFileDialog />', () => {
118
140
  });
119
141
  });
120
142
  it('handles errors', () => {
121
- store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
122
143
  loadDataAndMount(
123
144
  'cypress/test_files/sequencing/cloning_strategy_linear.json',
124
145
  store,
125
146
  () => {
126
- cy.mount(<VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />);
147
+ cy.mount(
148
+ <Provider store={store}>
149
+ <ConfigProvider config={config}>
150
+ <VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />
151
+ </ConfigProvider>
152
+ </Provider>
153
+ );
127
154
  },
128
155
  ).then(() => {
129
156
  // Error if submitting non-allowed files
@@ -1,7 +1,8 @@
1
- import { useSelector } from 'react-redux';
1
+ import { useConfig } from './useConfig';
2
2
 
3
3
  export default function useBackendRoute() {
4
- const configBackendUrl = useSelector((state) => state.cloning.config.backendUrl);
4
+ const { backendUrl: configBackendUrl } = useConfig();
5
+
5
6
  if (!configBackendUrl) {
6
7
  return () => {};
7
8
  }
@@ -0,0 +1,2 @@
1
+ // Re-export useConfig from ConfigProvider for convenience
2
+ export { useConfig } from '../providers/ConfigProvider';
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import { useSelector } from 'react-redux';
2
+ import { useConfig } from './useConfig';
3
3
  import eLabFTWInterface from '../components/eLabFTW/eLabFTWInterface';
4
4
  import dummyInterface from '../components/dummy/DummyInterface';
5
5
 
6
6
  export default function useDatabase() {
7
- const databaseName = useSelector((state) => state.cloning.config.database);
7
+ const { database: databaseName } = useConfig();
8
8
 
9
9
  return React.useMemo(() => {
10
10
  if (databaseName === 'elabftw') {
@@ -1,12 +1,18 @@
1
- import { useSelector } from 'react-redux';
2
1
  import React from 'react';
3
2
  import getHttpClient from '@opencloning/utils/getHttpClient';
3
+ import { useConfig } from './useConfig';
4
4
 
5
5
  export default function useHttpClient() {
6
- const backendUrl = useSelector((state) => state.cloning.config.backendUrl);
6
+ const { backendUrl } = useConfig();
7
7
 
8
8
  // Memoize the client creation and interceptor setup
9
- const apiClient = React.useMemo(() => getHttpClient([backendUrl]), [backendUrl]);
9
+ const apiClient = React.useMemo(() => {
10
+ if (!backendUrl) {
11
+ // Return a client without backend URL if config not loaded yet
12
+ return getHttpClient([]);
13
+ }
14
+ return getHttpClient([backendUrl]);
15
+ }, [backendUrl]);
10
16
 
11
17
  return apiClient;
12
18
  }
@@ -0,0 +1,15 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * Hook to initialize application-level concerns
5
+ * - Clears session storage
6
+ */
7
+ export default function useInitializeApp() {
8
+
9
+ useEffect(() => {
10
+ // Clear session storage
11
+ // eslint-disable-next-line no-undef
12
+ sessionStorage.clear();
13
+ }, []);
14
+ }
15
+
@@ -0,0 +1,149 @@
1
+ import { useEffect } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { cloningActions } from '@opencloning/store/cloning';
4
+ import useDatabase from './useDatabase';
5
+ import useLoadDatabaseFile from './useLoadDatabaseFile';
6
+ import useAlerts from './useAlerts';
7
+ import useHttpClient from './useHttpClient';
8
+ import useValidateState from './useValidateState';
9
+ import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
10
+ import { formatTemplate, loadHistoryFile, loadFilesToSessionStorage } from '@opencloning/utils/readNwrite';
11
+
12
+ const { setState: setCloningState, updateSource } = cloningActions;
13
+
14
+ /**
15
+ * Hook to load sequences from URL parameters
16
+ * Handles various source types: database, example, template, genome_coordinates, locus_tag
17
+ */
18
+ export default function useUrlParamsLoader() {
19
+ const dispatch = useDispatch();
20
+ const database = useDatabase();
21
+ const { addAlert } = useAlerts();
22
+ const setHistoryFileError = (e) => addAlert({ message: e, severity: 'error' });
23
+ const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendPostRequest: null, setHistoryFileError });
24
+ const validateState = useValidateState();
25
+ const httpClient = useHttpClient();
26
+
27
+ useEffect(() => {
28
+ async function loadSequenceFromUrlParams() {
29
+ const urlParams = getUrlParameters();
30
+
31
+ if (urlParams.source === 'database') {
32
+ try {
33
+ if (!database) {
34
+ return;
35
+ }
36
+ const { file, databaseId } = await database.loadSequenceFromUrlParams(urlParams);
37
+ loadDatabaseFile(file, databaseId);
38
+ } catch (error) {
39
+ addAlert({
40
+ message: 'Error loading sequence from URL parameters',
41
+ severity: 'error',
42
+ });
43
+ console.error(error);
44
+ }
45
+ } else if (urlParams.source === 'example' && urlParams.example) {
46
+ try {
47
+ const url = `${import.meta.env.BASE_URL}examples/${urlParams.example}`;
48
+ let data;
49
+ if (urlParams.example.endsWith('.zip')) {
50
+ // For zip files, get as blob and process with loadHistoryFile
51
+ const { data: blob } = await httpClient.get(url, { responseType: 'blob' });
52
+ const fileName = urlParams.example;
53
+ // eslint-disable-next-line no-undef
54
+ const file = new File([blob], fileName);
55
+ const { cloningStrategy, verificationFiles } = await loadHistoryFile(file);
56
+ data = await validateState(cloningStrategy);
57
+ await loadFilesToSessionStorage(verificationFiles, 0);
58
+ } else {
59
+ // For JSON files, get as JSON
60
+ const { data: jsonData } = await httpClient.get(url);
61
+ data = await validateState(jsonData);
62
+ }
63
+ dispatch(setCloningState(data));
64
+ } catch (error) {
65
+ addAlert({
66
+ message: 'Error loading example',
67
+ severity: 'error',
68
+ });
69
+ console.error(error);
70
+ }
71
+ } else if (urlParams.source === 'template' && urlParams.template && urlParams.key) {
72
+ try {
73
+ const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
74
+ const url = `${baseUrl}/processed/${urlParams.key}/templates/${urlParams.template}`;
75
+ const { data } = await httpClient.get(url);
76
+ const validatedData = await validateState(data);
77
+ const newState = formatTemplate(validatedData, url);
78
+
79
+ dispatch(setCloningState(newState));
80
+ } catch (error) {
81
+ addAlert({
82
+ message: 'Error loading template',
83
+ severity: 'error',
84
+ });
85
+ console.error(error);
86
+ }
87
+ } else if (urlParams.source === 'genome_coordinates') {
88
+ const { sequence_accession, start, end, strand, assembly_accession } = urlParams;
89
+ if (!sequence_accession || !start || !end || !strand) {
90
+ addAlert({
91
+ message: 'Error loading genome sequence from URL parameters',
92
+ severity: 'error',
93
+ });
94
+ return;
95
+ }
96
+ const startNum = Number(start);
97
+ const endNum = Number(end);
98
+ const strandNum = Number(strand);
99
+ let error = '';
100
+ if (isNaN(startNum) || isNaN(endNum)) {
101
+ error = 'Start and end must be numbers';
102
+ }
103
+ else if (![1, -1].includes(strandNum)) {
104
+ error = 'Strand must be 1 or -1';
105
+ }
106
+ else if (startNum < 1) {
107
+ error = 'Start must be greater than zero';
108
+ }
109
+ else if (startNum >= endNum) {
110
+ error = 'End must be greater than start';
111
+ }
112
+ if (error) {
113
+ addAlert({ message: `Error loading genome coordinates from URL parameters: ${error}`, severity: 'error' });
114
+ return;
115
+ }
116
+
117
+ const source = {
118
+ id: 1,
119
+ type: 'KnownGenomeCoordinatesSource',
120
+ assembly_accession,
121
+ repository_id: sequence_accession,
122
+ coordinates: formatSequenceLocationString(start, end, strand),
123
+ }
124
+ dispatch(updateSource(source));
125
+ } else if (urlParams.source === 'locus_tag') {
126
+ const { locus_tag, assembly_accession, padding } = urlParams;
127
+ if (!locus_tag || !assembly_accession) {
128
+ addAlert({
129
+ message: 'Error loading locus tag from URL parameters',
130
+ severity: 'error',
131
+ });
132
+ return;
133
+ }
134
+
135
+ const source = {
136
+ id: 1,
137
+ type: 'KnownGenomeCoordinatesSource',
138
+ assembly_accession,
139
+ locus_tag,
140
+ padding: padding ? Number(padding) : 1000,
141
+ }
142
+ dispatch(updateSource(source));
143
+ }
144
+ }
145
+ loadSequenceFromUrlParams();
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, []);
148
+ }
149
+
package/src/index.css ADDED
@@ -0,0 +1,314 @@
1
+ button {
2
+ cursor: pointer;
3
+ }
4
+
5
+ .tf-tree .tf-nc, .tf-tree .tf-node-content {
6
+ border: 3px solid;
7
+ border-color: rgb(25, 118, 210);
8
+ }
9
+
10
+ ul.hidden-ancestors {
11
+ display: none;
12
+ }
13
+
14
+ /* So that the eye icon is visible */
15
+ li.hidden-ancestors {
16
+ margin-bottom: 30px;
17
+ }
18
+
19
+ .app-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ /* height: calc(100vh - 114px - 10px); - moved to component to depend on presence of appbar */
23
+ background-color: white;
24
+ }
25
+
26
+ .tab-panels-container {
27
+ height: 100%;
28
+ overflow: auto;
29
+ }
30
+
31
+ .main-sequence-editor {
32
+ width: 70%;
33
+ margin: auto;
34
+ }
35
+
36
+
37
+ .node-corner {
38
+ position: absolute;
39
+ right: -1px;
40
+ top: 1px;
41
+ display: flex;
42
+ gap: .2em;
43
+ }
44
+
45
+ .overhang-representation {
46
+ display: inline-block;
47
+ margin-top: 10px;
48
+ font-family: monospace;
49
+ font-size: small;
50
+ /* Prevent the removal of spaces that make sequences align correctly */
51
+ white-space: pre;
52
+ max-width: 300px;
53
+ overflow-x: auto;
54
+ }
55
+
56
+ div.before-node {
57
+ position: absolute;
58
+ top: 0;
59
+ left: 50%;
60
+ transform: translate(-50%, -38px);
61
+ z-index: 1;
62
+ }
63
+
64
+ div.before-node svg {
65
+ background-color: white;
66
+ cursor: pointer;
67
+ }
68
+
69
+ div.hang-from-node {
70
+ box-sizing: content-box;
71
+ height: 50px;
72
+ width: 50px;
73
+ position: absolute;
74
+ top: 100%;
75
+ left: 50%;
76
+ transform: translate(-50%, 40px);
77
+ border: 3px solid;
78
+ border-color: rgb(25, 118, 210);
79
+ border-radius: 100%;
80
+ }
81
+
82
+ /* To center the icon vertically */
83
+ div.hang-from-node div {
84
+ margin: 0;
85
+ position: absolute;
86
+ top: 50%;
87
+ left: 50%;
88
+ transform: translate(-50%, -50%);
89
+ }
90
+
91
+ div.hang-from-node::before {
92
+ position: absolute;
93
+ border: 1px solid;
94
+ width: 0;
95
+ height: 30px;
96
+ display: block;
97
+ content: '';
98
+ top: -3%;
99
+ left: 50%;
100
+ margin-left: -1px;
101
+ transform: translate(0, -100%);
102
+ }
103
+
104
+
105
+ /* TODO: handle these */
106
+ div.tf-tree.tf-ancestor-tree {
107
+ padding-bottom: 150px;
108
+ }
109
+
110
+ div.corner-id {
111
+ position: absolute;
112
+ left: 0px;
113
+ top: 0px;
114
+ font-family: sans-serif;
115
+ font-size: large;
116
+ color: rgb(25, 118, 210);
117
+ font-weight: bold;
118
+ }
119
+
120
+ button.github-icon svg {
121
+ padding: 0;
122
+ color: white;
123
+ font-size: 30px;
124
+ }
125
+
126
+ div.collapsed div {
127
+ margin: auto
128
+ }
129
+
130
+ div.collapsed button {
131
+ margin-bottom: 0px;
132
+ margin-top: 0px;
133
+ }
134
+
135
+ /* Prevents scollbar from showing on small editors,
136
+ that was caused by overflowing numbers at the edges
137
+ of the x axis */
138
+
139
+ span.node-text svg.veAxis {
140
+ width: 100%;
141
+ }
142
+
143
+ span.node-text > div:first-child {
144
+ margin: 20px
145
+ }
146
+
147
+ /* Cannot overwrite styles that are hard-coded, but we may want this */
148
+ /* span.node-text div.tg-simple-dna-view {
149
+ overflow: visible;
150
+
151
+ } */
152
+
153
+ body {
154
+ margin: 0px;
155
+ padding: 0px;
156
+ height: 100%;
157
+ overflow: hidden;
158
+ display: flex;
159
+ flex-direction: column;
160
+ }
161
+
162
+ html {
163
+ overflow: hidden;
164
+ }
165
+
166
+ div.select-source div.MuiFormControl-root {
167
+ margin-top: 5px;
168
+ margin-bottom: 5px;
169
+ }
170
+
171
+ div.select-source h2 {
172
+ margin-bottom: 5px;
173
+ font-size: x-large;
174
+ }
175
+
176
+
177
+ div.select-source {
178
+ width: 275px;
179
+ }
180
+
181
+ .fragment-picker {
182
+ margin-top: 5px;
183
+ margin-bottom: 10px;
184
+ text-align: center;
185
+ }
186
+
187
+ div.description-container {
188
+ margin: auto;
189
+ margin-bottom: 2em;
190
+ max-width: 600px;
191
+ }
192
+
193
+ div.description-container p {
194
+ /* Show line breaks */
195
+ white-space: pre-line;
196
+ text-align: left;
197
+ }
198
+
199
+ code {
200
+ background-color: lightgrey;
201
+ padding: 15px;
202
+ border-radius: 5px;
203
+ min-width: 40%;
204
+ box-shadow: inset 0px 0px 0px 3px gray;
205
+ margin-bottom: 30px;
206
+ white-space: pre-wrap;
207
+ display: inline-block;
208
+ text-align: left;
209
+ max-width: 50%;
210
+ }
211
+
212
+ .data-model-displayer {
213
+ text-align: center;
214
+ }
215
+
216
+ .data-model-displayer p {
217
+ font-size: large;
218
+ }
219
+
220
+ a.button-hyperlink {
221
+ text-decoration: none;
222
+ color: white;
223
+ }
224
+
225
+ div.dragging-file {
226
+ text-align: center;
227
+ }
228
+
229
+ .drag-file-wrapper {
230
+ border: 5px dashed;
231
+ border-color: darkgray;
232
+ width: 40%;
233
+ height: 400px;
234
+ padding: 10px;
235
+ margin: auto;
236
+ text-align: center;
237
+ border-radius: 40px;
238
+ }
239
+
240
+ .drag-file-container {
241
+ padding: 10px;
242
+ }
243
+
244
+ .drag-file-container .drag-file-close {
245
+ text-align: right;
246
+ color: red;
247
+ cursor: pointer;
248
+ }
249
+
250
+ .drag-file-container .drag-file-close svg {
251
+ font-size: 40px;
252
+ }
253
+
254
+ .drag-file-container .drag-file-close:hover {
255
+ /* 70% brightness */
256
+ filter: brightness(70%);
257
+ }
258
+
259
+ div.primer-designer {
260
+ margin: auto;
261
+ width: 60%;
262
+ }
263
+
264
+
265
+ div.pcr-unit.MuiAccordion-root:first-of-type {
266
+ border-radius: 1.2em;
267
+ overflow: clip;
268
+ }
269
+
270
+ div.pcr-unit.MuiAccordion-root {
271
+ border-radius: 1.2em;
272
+ overflow: clip;
273
+ margin-bottom: .5em;
274
+ }
275
+
276
+ .pcr-unit div.MuiAccordionSummary-root.MuiAccordionSummary-root {
277
+ min-height: 0;
278
+ }
279
+
280
+ .pcr-unit div.MuiAccordionSummary-content {
281
+ margin-top: .35em;
282
+ margin-bottom: .35em;
283
+ }
284
+
285
+ /* Vertical align the content and icon in alert banners */
286
+ div.MuiPaper-root[role=alert] {
287
+ align-items: center;
288
+ }
289
+
290
+ div#global-error-message-wrapper {
291
+ position: fixed;
292
+ top: 0;
293
+ z-index: 999;
294
+ max-width: 25em;
295
+ }
296
+
297
+ div#global-error-message-wrapper > div {
298
+ margin: 10px;
299
+ /* Allow line breaks in very long strings only when necessary */
300
+ word-break: break-word;
301
+ }
302
+
303
+ .finished-source {
304
+ /* Allow line breaks in very long strings only when necessary */
305
+ word-break: break-word;
306
+ }
307
+
308
+ .tab-panels-container {
309
+ text-align: center;
310
+ }
311
+
312
+ div[role="tooltip"] div.MuiTooltip-tooltip {
313
+ font-size: 1em;
314
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Import styles - these will be applied when the package is used
2
+ import './index.css';
3
+
4
+ // Re-export components
5
+ export * from './components/index.js';
6
+ // Export version - replaced at publish time via prepack script
7
+ export { version } from './version.js';
8
+
@@ -0,0 +1,51 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+
3
+ const ConfigContext = createContext(null);
4
+
5
+ /**
6
+ * ConfigProvider - Provides application configuration via React Context
7
+ *
8
+ * @param {Object} props
9
+ * @param {Object} props.config - Config object
10
+ * @param {React.ReactNode} props.children - Child components
11
+ */
12
+ export function ConfigProvider({ config, children }) {
13
+ if (!config) {
14
+ throw new Error('ConfigProvider requires a config prop');
15
+ }
16
+
17
+ const value = useMemo(() => ({
18
+ config,
19
+ }), [config]);
20
+
21
+ return (
22
+ <ConfigContext.Provider value={value}>
23
+ {children}
24
+ </ConfigContext.Provider>
25
+ );
26
+ }
27
+
28
+ /**
29
+ * useConfig - Hook to access configuration from ConfigProvider
30
+ *
31
+ * @returns {Object} Configuration object with properties:
32
+ * - backendUrl: string
33
+ * - showAppBar: boolean
34
+ * - enableAssembler: boolean
35
+ * - enablePlannotate: boolean
36
+ * - noExternalRequests: boolean
37
+ * - database: string | null
38
+ *
39
+ * @throws {Error} If used outside of ConfigProvider
40
+ */
41
+ export function useConfig() {
42
+ const context = useContext(ConfigContext);
43
+
44
+ if (context === null) {
45
+ throw new Error('useConfig must be used within a ConfigProvider');
46
+ }
47
+
48
+ return context.config;
49
+ }
50
+
51
+ export default ConfigProvider;
package/src/version.js ADDED
@@ -0,0 +1,2 @@
1
+ // Version placeholder - replaced at publish time via prepack script
2
+ export const version = "1.1.0-test.2";
@@ -1,50 +0,0 @@
1
- import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
2
- import React from 'react';
3
-
4
- function KnownSourceErrors({ errors }) {
5
- const [dialogOpen, setDialogOpen] = React.useState(false);
6
- return (
7
- <>
8
- <Alert
9
- severity="error"
10
- action={(
11
- <Button color="inherit" size="small" onClick={() => setDialogOpen(true)}>
12
- See how
13
- </Button>
14
- )}
15
- sx={{ alignItems: 'center', mb: 1 }}
16
- >
17
- Affected by external errors
18
- </Alert>
19
- <Dialog
20
- open={dialogOpen}
21
- onClose={() => setDialogOpen(false)}
22
- >
23
- <DialogTitle>Known external errors</DialogTitle>
24
- <DialogContent>
25
- <ul>
26
- {errors.map((error, i) => (
27
- <li style={{ marginBottom: '1em' }} key={i} component="li">
28
- {error}
29
- </li>
30
- ))}
31
- </ul>
32
-
33
- </DialogContent>
34
- <DialogActions>
35
- <Button
36
- onClick={() => {
37
- setDialogOpen(false);
38
- }}
39
- >
40
- Close
41
- </Button>
42
-
43
- </DialogActions>
44
- </Dialog>
45
- </>
46
-
47
- );
48
- }
49
-
50
- export default KnownSourceErrors;