@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 +24 -0
- package/package.json +10 -4
- package/scripts/inject-version.js +17 -0
- package/scripts/reset-version.js +16 -0
- package/src/components/ExternalServicesStatusCheck.jsx +2 -1
- package/src/components/OpenCloning.jsx +2 -2
- package/src/components/form/EnzymeMultiSelect.cy.jsx +20 -9
- package/src/components/primers/PrimerList.cy.jsx +86 -78
- package/src/components/sources/Source.jsx +0 -3
- package/src/components/sources/SourceTypeSelector.jsx +2 -2
- package/src/components/verification/VerificationFileDialog.cy.jsx +35 -8
- package/src/hooks/useBackendRoute.js +3 -2
- package/src/hooks/useConfig.js +2 -0
- package/src/hooks/useDatabase.js +2 -2
- package/src/hooks/useHttpClient.js +9 -3
- package/src/hooks/useInitializeApp.js +15 -0
- package/src/hooks/useUrlParamsLoader.js +149 -0
- package/src/index.css +314 -0
- package/src/index.js +8 -0
- package/src/providers/ConfigProvider.jsx +51 -0
- package/src/version.js +2 -0
- package/src/components/sources/KnownSourceErrors.jsx +0 -50
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
|
|
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
|
|
21
|
-
"@opencloning/utils": "1.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:
|
|
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 =
|
|
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
|
|
4
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
1
|
+
import { useConfig } from './useConfig';
|
|
2
2
|
|
|
3
3
|
export default function useBackendRoute() {
|
|
4
|
-
const configBackendUrl =
|
|
4
|
+
const { backendUrl: configBackendUrl } = useConfig();
|
|
5
|
+
|
|
5
6
|
if (!configBackendUrl) {
|
|
6
7
|
return () => {};
|
|
7
8
|
}
|
package/src/hooks/useDatabase.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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 =
|
|
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 =
|
|
6
|
+
const { backendUrl } = useConfig();
|
|
7
7
|
|
|
8
8
|
// Memoize the client creation and interceptor setup
|
|
9
|
-
const apiClient = React.useMemo(() =>
|
|
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
|
@@ -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;
|