@rivm-loculus-tools/common-frontend 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/LoculusService.js +1049 -0
- package/dist/_chunks/LoculusService.js.map +1 -0
- package/dist/_chunks/decorateParam.js +7 -0
- package/dist/_chunks/keycloak-dpop.js +241 -0
- package/dist/_chunks/keycloak-dpop.js.map +1 -0
- package/dist/classes/services/EmotionCacheService/index.js +27 -1
- package/dist/classes/services/EmotionCacheService/index.js.map +1 -0
- package/dist/classes/services/LoculusService/LoculusService.d.ts +26 -0
- package/dist/classes/services/LoculusService/index.d.ts +1 -0
- package/dist/classes/services/LoculusService/index.js +1 -0
- package/dist/classes/services/UrlService/UrlService.d.ts +18 -0
- package/dist/classes/services/UrlService/index.d.ts +1 -0
- package/dist/classes/services/UrlService/index.js +45 -0
- package/dist/classes/services/UrlService/index.js.map +1 -0
- package/dist/components/app/App/App.d.ts +4 -2
- package/dist/components/app/App/index.js +35 -34
- package/dist/components/app/App/index.js.map +1 -1
- package/dist/components/authentication/AuthenticationLoader/AuthenticationLoader.d.ts +3 -0
- package/dist/components/authentication/AuthenticationLoader/index.d.ts +1 -0
- package/dist/components/authentication/AuthenticationLoader/index.js +31 -0
- package/dist/components/authentication/AuthenticationLoader/index.js.map +1 -0
- package/dist/components/fasta/FastaDataImportDialog/FastaDataImportDialog.d.ts +1 -0
- package/dist/components/fasta/FastaDataImportDialog/index.js +77 -77
- package/dist/components/fasta/FastaDataImportDialog/index.js.map +1 -1
- package/dist/components/fasta/FastaDataImportDialogLoader/FastaDataImportDialogLoader.d.ts +2 -1
- package/dist/components/fasta/FastaDataImportDialogLoader/index.js +61 -52
- package/dist/components/fasta/FastaDataImportDialogLoader/index.js.map +1 -1
- package/dist/components/fasta/FastaPageHeaderResetButton/index.js +27 -24
- package/dist/components/fasta/FastaPageHeaderResetButton/index.js.map +1 -1
- package/dist/components/loculus/UploadToLoculusModal/UploadToLoculusModal.d.ts +11 -0
- package/dist/components/loculus/UploadToLoculusModal/index.d.ts +1 -0
- package/dist/components/loculus/UploadToLoculusModal/index.js +159 -0
- package/dist/components/loculus/UploadToLoculusModal/index.js.map +1 -0
- package/dist/components/ui/ButtonWithMenu/ButtonWithMenu.d.ts +1 -0
- package/dist/components/ui/ButtonWithMenu/index.js +5 -4
- package/dist/components/ui/ButtonWithMenu/index.js.map +1 -1
- package/dist/components/ui/PageCommon/index.js +1 -1
- package/dist/components/ui/PageCommon/index.js.map +1 -1
- package/dist/components/ui/SearchBar/index.js +1 -0
- package/dist/components/ui/SearchBar/index.js.map +1 -1
- package/dist/models/url.models.d.ts +9 -0
- package/dist/models/url.models.js +0 -0
- package/dist/utils/MetaDataUtil/MetaDataUtil.d.ts +2 -1
- package/dist/utils/MetaDataUtil/index.js +5 -2
- package/dist/utils/MetaDataUtil/index.js.map +1 -1
- package/dist/utils/UrlUtil/UrlUtil.d.ts +1 -1
- package/dist/utils/UrlUtil/index.js +5 -6
- package/dist/utils/UrlUtil/index.js.map +1 -1
- package/package.json +23 -2
- package/dist/_chunks/EmotionCacheService.js +0 -32
- package/dist/_chunks/EmotionCacheService.js.map +0 -1
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import { UrlUtil as e } from "../../../utils/UrlUtil/index.js";
|
|
2
|
+
import { t } from "../../../_chunks/LoculusService.js";
|
|
3
|
+
import { container as n } from "tsyringe";
|
|
4
|
+
import { IconButton as r, Tooltip as i } from "@mui/material";
|
|
5
|
+
import { t as a } from "i18next";
|
|
6
|
+
import { Fragment as o, jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
7
|
+
import { useCallback as l, useRef as u } from "react";
|
|
8
|
+
import d from "@mui/icons-material/RestartAlt";
|
|
9
|
+
import { Confirmation as f } from "@gen-epix/ui-core-components/components/Confirmation";
|
|
7
10
|
// src/components/fasta/FastaPageHeaderResetButton/FastaPageHeaderResetButton.tsx
|
|
8
|
-
var
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
}, []),
|
|
12
|
-
|
|
13
|
-
}, [
|
|
14
|
-
return /* @__PURE__ */
|
|
11
|
+
var p = ({ onReset: p }) => {
|
|
12
|
+
let m = u(null), h = l(() => {
|
|
13
|
+
m.current?.open();
|
|
14
|
+
}, []), g = l(() => {
|
|
15
|
+
e.clearSearchParams(), n.resolve(t).clear(), p();
|
|
16
|
+
}, [p]);
|
|
17
|
+
return /* @__PURE__ */ c(o, { children: [/* @__PURE__ */ s(i, {
|
|
15
18
|
title: "Reset current session and dataset",
|
|
16
|
-
children: /* @__PURE__ */
|
|
19
|
+
children: /* @__PURE__ */ s(r, {
|
|
17
20
|
color: "inherit",
|
|
18
|
-
onClick:
|
|
21
|
+
onClick: h,
|
|
19
22
|
size: "small",
|
|
20
23
|
sx: { opacity: .85 },
|
|
21
|
-
children: /* @__PURE__ */
|
|
24
|
+
children: /* @__PURE__ */ s(d, { fontSize: "small" })
|
|
22
25
|
})
|
|
23
|
-
}), /* @__PURE__ */
|
|
24
|
-
body:
|
|
25
|
-
cancelLabel:
|
|
26
|
-
confirmLabel:
|
|
27
|
-
onConfirm:
|
|
28
|
-
ref:
|
|
29
|
-
title:
|
|
26
|
+
}), /* @__PURE__ */ s(f, {
|
|
27
|
+
body: a`Are you sure you want to reset the current session and dataset?`,
|
|
28
|
+
cancelLabel: a`Cancel`,
|
|
29
|
+
confirmLabel: a`Reset`,
|
|
30
|
+
onConfirm: g,
|
|
31
|
+
ref: m,
|
|
32
|
+
title: a`Reset current session and dataset?`
|
|
30
33
|
})] });
|
|
31
34
|
};
|
|
32
|
-
export {
|
|
35
|
+
export { p as FastaPageHeaderResetButton };
|
|
33
36
|
|
|
34
37
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_Fragment","RestartAltIcon","FastaPageHeaderResetButton","confirmationRef","useRef","onClickResetSession","useCallback","onConfirm","onReset","Fragment","Tooltip","IconButton","Confirmation"],"sources":["../../../../src/components/fasta/FastaPageHeaderResetButton/FastaPageHeaderResetButton.tsx"],"sourcesContent":["import RestartAltIcon from '@mui/icons-material/RestartAlt';\nimport {\n IconButton,\n Tooltip,\n} from '@mui/material';\nimport {\n useCallback,\n useRef,\n} from 'react';\nimport { Confirmation } from '@gen-epix/ui-core-components/components/Confirmation';\nimport type { ConfirmationRefMethods } from '@gen-epix/ui-core-components/components/Confirmation';\nimport { t } from 'i18next';\n\nexport type FastaPageHeaderResetButtonProps = {\n onReset: () => void;\n};\n\nexport const FastaPageHeaderResetButton = ({ onReset }: FastaPageHeaderResetButtonProps) => {\n const confirmationRef = useRef<ConfirmationRefMethods>(null);\n const onClickResetSession = useCallback(() => {\n confirmationRef.current?.open();\n }, []);\n\n const onConfirm = useCallback(() => {\n onReset();\n }, [onReset]);\n\n return (\n <>\n <Tooltip title={'Reset current session and dataset'}>\n <IconButton\n color={'inherit'}\n onClick={onClickResetSession}\n size={'small'}\n sx={{ opacity: 0.85 }}\n >\n <RestartAltIcon fontSize={'small'} />\n </IconButton>\n </Tooltip>\n <Confirmation\n body={t`Are you sure you want to reset the current session and dataset?`}\n cancelLabel={t`Cancel`}\n confirmLabel={t`Reset`}\n onConfirm={onConfirm}\n ref={confirmationRef}\n title={t`Reset current session and dataset?`}\n />\n </>\n );\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment","RestartAltIcon","FastaPageHeaderResetButton","confirmationRef","useRef","onClickResetSession","useCallback","onConfirm","UrlUtil","container","LoculusService","onReset","Fragment","Tooltip","IconButton","Confirmation"],"sources":["../../../../src/components/fasta/FastaPageHeaderResetButton/FastaPageHeaderResetButton.tsx"],"sourcesContent":["import RestartAltIcon from '@mui/icons-material/RestartAlt';\nimport {\n IconButton,\n Tooltip,\n} from '@mui/material';\nimport {\n useCallback,\n useRef,\n} from 'react';\nimport { Confirmation } from '@gen-epix/ui-core-components/components/Confirmation';\nimport type { ConfirmationRefMethods } from '@gen-epix/ui-core-components/components/Confirmation';\nimport { t } from 'i18next';\nimport { container } from 'tsyringe';\n\nimport { UrlUtil } from '../../../utils/UrlUtil';\nimport { LoculusService } from '../../../classes/services/LoculusService';\n\nexport type FastaPageHeaderResetButtonProps = {\n onReset: () => void;\n};\n\nexport const FastaPageHeaderResetButton = ({ onReset }: FastaPageHeaderResetButtonProps) => {\n const confirmationRef = useRef<ConfirmationRefMethods>(null);\n const onClickResetSession = useCallback(() => {\n confirmationRef.current?.open();\n }, []);\n\n const onConfirm = useCallback(() => {\n UrlUtil.clearSearchParams();\n container.resolve(LoculusService).clear();\n onReset();\n }, [onReset]);\n\n return (\n <>\n <Tooltip title={'Reset current session and dataset'}>\n <IconButton\n color={'inherit'}\n onClick={onClickResetSession}\n size={'small'}\n sx={{ opacity: 0.85 }}\n >\n <RestartAltIcon fontSize={'small'} />\n </IconButton>\n </Tooltip>\n <Confirmation\n body={t`Are you sure you want to reset the current session and dataset?`}\n cancelLabel={t`Cancel`}\n confirmLabel={t`Reset`}\n onConfirm={onConfirm}\n ref={confirmationRef}\n title={t`Reset current session and dataset?`}\n />\n </>\n );\n};\n"],"mappings":";;;;;;;;;;AAqBA,IAAaE,KAA8B,EAAE,iBAA+C;CAC1F,IAAMC,IAAkBC,EAA+B,IAAI,GACrDC,IAAsBC,QAAkB;EAC5C,EAAgB,SAAS,KAAK;CAChC,GAAG,CAAC,CAAC,GAECC,IAAYD,QAAkB;EAGlC,AAFAE,EAAQ,kBAAkB,GAC1BC,EAAU,QAAQC,CAAc,CAAC,CAAC,MAAM,GACxCC,EAAQ;CACV,GAAG,CAACA,CAAO,CAAC;CAEZ,OACE,kBAAAX,GAAA,EAAA,UAAA,CACE,kBAACa,GAAD;EAAS,OAAO;EACd,UAAA,kBAACC,GAAD;GACE,OAAO;GACP,SAAST;GACT,MAAM;GACN,IAAI,EAAE,SAAS,IAAK;GAEpB,UAAA,kBAACJ,GAAD,EAAgB,UAAU,QAAU,CAAA;EAC1B,CAAA;CACL,CAAA,GACT,kBAACc,GAAD;EACE,MAAM,CAAC;EACP,aAAa,CAAC;EACd,cAAc,CAAC;EACJ;EACX,KAAKZ;EACL,OAAO,CAAC;CACT,CAAA,CACD,EAAA,CAAA;AAEN"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WithDialogRefMethods, WithDialogRenderProps } from '@gen-epix/ui-core-components/hoc/withDialog';
|
|
2
|
+
import { MetadataParseResult } from '../../../models/metadata.models';
|
|
3
|
+
export interface UploadToLoculusModalOpenProps {
|
|
4
|
+
fasta: string;
|
|
5
|
+
metaData: MetadataParseResult | null;
|
|
6
|
+
rawFasta: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UploadToLoculusModalProps extends WithDialogRenderProps<UploadToLoculusModalOpenProps> {
|
|
9
|
+
}
|
|
10
|
+
export type UploadToLoculusModalRefMethods = WithDialogRefMethods<UploadToLoculusModalProps, UploadToLoculusModalOpenProps>;
|
|
11
|
+
export declare const UploadToLoculusModal: import('react').ComponentClass<UploadToLoculusModalProps, import('@gen-epix/ui-core-components/hoc/withDialog').WithDialogState<UploadToLoculusModalOpenProps>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './UploadToLoculusModal';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { FastaUtil as e } from "../../../utils/FastaUtil/index.js";
|
|
2
|
+
import { t } from "../../../_chunks/LoculusService.js";
|
|
3
|
+
import { UrlService as n } from "../../../classes/services/UrlService/index.js";
|
|
4
|
+
import { container as r } from "tsyringe";
|
|
5
|
+
import { Alert as i, Box as a, Paper as o, Table as s, TableBody as c, TableCell as l, TableContainer as u, TableHead as d, TableRow as f } from "@mui/material";
|
|
6
|
+
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
7
|
+
import { useCallback as g, useEffect as _, useMemo as v, useRef as y, useState as b } from "react";
|
|
8
|
+
import { Spinner as x } from "@gen-epix/ui-core-components/components/Spinner";
|
|
9
|
+
import { withDialog as S } from "@gen-epix/ui-core-components/hoc/withDialog";
|
|
10
|
+
// src/components/loculus/UploadToLoculusModal/UploadToLoculusModal.tsx
|
|
11
|
+
var C = S(({ onActionsChange: S, onClose: C, openProps: w }) => {
|
|
12
|
+
let T = v(() => r.resolve(t), []), E = v(() => r.resolve(n), []), [D, O] = b(!1), [k, A] = b(!1), [j, M] = b(null), N = y(null), P = w?.fasta ?? "", F = w?.metaData ?? null, I = w?.rawFasta ?? "";
|
|
13
|
+
_(() => () => {
|
|
14
|
+
N.current?.abort();
|
|
15
|
+
}, []);
|
|
16
|
+
let L = v(() => {
|
|
17
|
+
let n = [];
|
|
18
|
+
T.data.authenticated || n.push("You must be authenticated to upload to Loculus."), E.hasReturnParams() || n.push("The Loculus return URL and organism must be provided."), F || n.push("Metadata must be loaded before uploading.");
|
|
19
|
+
let r = e.parseFasta(P);
|
|
20
|
+
if (r.error ? n.push(r.error) : r.sequences.length === 0 && n.push("At least one FASTA sequence is required."), F?.error && n.push(F.error), n.length > 0 || !F) return {
|
|
21
|
+
errors: n,
|
|
22
|
+
revisedSequences: null
|
|
23
|
+
};
|
|
24
|
+
try {
|
|
25
|
+
let e = t.createRevisedSequences(P, F, I);
|
|
26
|
+
return e.sequenceCount === 0 && n.push("No FASTA sequences match the metadata."), {
|
|
27
|
+
errors: n,
|
|
28
|
+
revisedSequences: e
|
|
29
|
+
};
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return n.push(e instanceof Error ? e.message : "Failed to prepare the upload preview."), {
|
|
32
|
+
errors: n,
|
|
33
|
+
revisedSequences: null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}, [
|
|
37
|
+
P,
|
|
38
|
+
T,
|
|
39
|
+
F,
|
|
40
|
+
I,
|
|
41
|
+
E
|
|
42
|
+
]), R = L.revisedSequences?.metadata.split("\n").slice(1).filter((e) => e.length > 0).reduce((e, t) => {
|
|
43
|
+
let n = e.filter((e) => e.key.startsWith(`${t}-`)).length;
|
|
44
|
+
return e.push({
|
|
45
|
+
key: `${t}-${n}`,
|
|
46
|
+
values: t.split(" ")
|
|
47
|
+
}), e;
|
|
48
|
+
}, []), z = g(async () => {
|
|
49
|
+
if (L.errors.length > 0 || !L.revisedSequences || !F || !E.hasReturnParams()) return;
|
|
50
|
+
M(null), O(!0);
|
|
51
|
+
let e = new AbortController();
|
|
52
|
+
N.current = e;
|
|
53
|
+
try {
|
|
54
|
+
let { organism: t, returnUrl: n } = E.data;
|
|
55
|
+
await T.reviseSequences(P, F, n, t, e), A(!0);
|
|
56
|
+
} catch (t) {
|
|
57
|
+
e.signal.aborted || M(t instanceof Error ? t.message : "Failed to upload sequences to Loculus.");
|
|
58
|
+
} finally {
|
|
59
|
+
e.signal.aborted || (N.current = null, O(!1));
|
|
60
|
+
}
|
|
61
|
+
}, [
|
|
62
|
+
P,
|
|
63
|
+
T,
|
|
64
|
+
F,
|
|
65
|
+
E,
|
|
66
|
+
L.errors.length,
|
|
67
|
+
L.revisedSequences
|
|
68
|
+
]), B = g(() => {
|
|
69
|
+
N.current?.abort(), N.current = null, O(!1), C?.();
|
|
70
|
+
}, [C]);
|
|
71
|
+
return _(() => {
|
|
72
|
+
let e = k ? [{
|
|
73
|
+
label: "Close tab",
|
|
74
|
+
onClick: () => window.close(),
|
|
75
|
+
variant: "contained"
|
|
76
|
+
}] : [...D ? [{
|
|
77
|
+
label: "Cancel",
|
|
78
|
+
onClick: B,
|
|
79
|
+
variant: "outlined"
|
|
80
|
+
}] : [], {
|
|
81
|
+
disabled: D || L.errors.length > 0 || L.revisedSequences === null,
|
|
82
|
+
label: j ? "Try again" : "Upload",
|
|
83
|
+
onClick: z,
|
|
84
|
+
variant: "contained"
|
|
85
|
+
}];
|
|
86
|
+
S?.(e);
|
|
87
|
+
}, [
|
|
88
|
+
k,
|
|
89
|
+
D,
|
|
90
|
+
S,
|
|
91
|
+
B,
|
|
92
|
+
z,
|
|
93
|
+
j,
|
|
94
|
+
L.errors.length,
|
|
95
|
+
L.revisedSequences
|
|
96
|
+
]), /* @__PURE__ */ m(a, {
|
|
97
|
+
sx: {
|
|
98
|
+
display: "grid",
|
|
99
|
+
gap: 2,
|
|
100
|
+
position: "relative"
|
|
101
|
+
},
|
|
102
|
+
children: D ? /* @__PURE__ */ m(x, {
|
|
103
|
+
inline: !0,
|
|
104
|
+
label: "Uploading sequences to Loculus"
|
|
105
|
+
}) : /* @__PURE__ */ h(p, { children: [
|
|
106
|
+
L.errors.length > 0 && /* @__PURE__ */ m(i, {
|
|
107
|
+
severity: "error",
|
|
108
|
+
children: L.errors.join(" ")
|
|
109
|
+
}),
|
|
110
|
+
j && /* @__PURE__ */ m(i, {
|
|
111
|
+
severity: "error",
|
|
112
|
+
children: j
|
|
113
|
+
}),
|
|
114
|
+
k && /* @__PURE__ */ m(i, {
|
|
115
|
+
severity: "success",
|
|
116
|
+
children: "Sequences successfully uploaded to Loculus. You can close this tab."
|
|
117
|
+
}),
|
|
118
|
+
!k && L.revisedSequences && /* @__PURE__ */ h(p, { children: [
|
|
119
|
+
/* @__PURE__ */ m(i, {
|
|
120
|
+
severity: "info",
|
|
121
|
+
children: `${L.revisedSequences.sequenceCount} sequence(s) and the matching metadata will be uploaded to Loculus.`
|
|
122
|
+
}),
|
|
123
|
+
L.revisedSequences.removedSequences.length > 0 && /* @__PURE__ */ m(i, {
|
|
124
|
+
severity: "warning",
|
|
125
|
+
children: `Sequences removed during editing and therefore not uploaded: ${L.revisedSequences.removedSequences.length}`
|
|
126
|
+
}),
|
|
127
|
+
L.revisedSequences.addedSequencesWithoutMetadata.length > 0 && /* @__PURE__ */ m(i, {
|
|
128
|
+
severity: "warning",
|
|
129
|
+
children: `Added sequences without matching metadata will not be uploaded: ${L.revisedSequences.addedSequencesWithoutMetadata.length}`
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ m(u, {
|
|
132
|
+
component: o,
|
|
133
|
+
sx: {
|
|
134
|
+
maxHeight: "400px",
|
|
135
|
+
overflowX: "auto"
|
|
136
|
+
},
|
|
137
|
+
children: /* @__PURE__ */ h(s, {
|
|
138
|
+
size: "small",
|
|
139
|
+
stickyHeader: !0,
|
|
140
|
+
sx: {
|
|
141
|
+
minWidth: "100%",
|
|
142
|
+
whiteSpace: "nowrap",
|
|
143
|
+
width: "max-content"
|
|
144
|
+
},
|
|
145
|
+
children: [/* @__PURE__ */ m(d, { children: /* @__PURE__ */ m(f, { children: F?.columns.map((e) => /* @__PURE__ */ m(l, { children: e }, e)) }) }), /* @__PURE__ */ m(c, { children: R?.map(({ key: e, values: t }) => /* @__PURE__ */ m(f, { children: t.map((t, n) => /* @__PURE__ */ m(l, { children: t }, `${e}-${F?.columns[n] ?? n}`)) }, e)) })]
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
] })
|
|
149
|
+
] })
|
|
150
|
+
});
|
|
151
|
+
}, {
|
|
152
|
+
defaultTitle: "Upload to Loculus",
|
|
153
|
+
fullWidth: !0,
|
|
154
|
+
maxWidth: "md",
|
|
155
|
+
testId: "UploadToLoculusModal"
|
|
156
|
+
});
|
|
157
|
+
export { C as UploadToLoculusModal };
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment","UploadToLoculusModal","withDialog","loculusService","useMemo","container","LoculusService","urlService","UrlService","isUploading","setIsUploading","useState","didUpload","setDidUpload","uploadError","setUploadError","abortControllerRef","useRef","fasta","openProps","metaData","rawFasta","validation","errors","parsedFasta","FastaUtil","revisedSequences","error","previewMetadataRows","row","occurrence","rows","existingRow","onClickUpload","useCallback","abortController","returnUrl","organism","onCancelUpload","onClose","useEffect","actions","onActionsChange","Box","Spinner","Fragment","Alert","TableContainer","Paper","Table","TableHead","TableRow","TableCell","column","TableBody","values","value","key","columnIndex"],"sources":["../../../../src/components/loculus/UploadToLoculusModal/UploadToLoculusModal.tsx"],"sourcesContent":["import {\n Alert,\n Box,\n Paper,\n Table,\n TableBody,\n TableCell,\n TableContainer,\n TableHead,\n TableRow,\n} from '@mui/material';\nimport type { DialogAction } from '@gen-epix/ui-core-components/components/Dialog';\nimport { Spinner } from '@gen-epix/ui-core-components/components/Spinner';\nimport type {\n WithDialogRefMethods,\n WithDialogRenderProps,\n} from '@gen-epix/ui-core-components/hoc/withDialog';\nimport { withDialog } from '@gen-epix/ui-core-components/hoc/withDialog';\nimport type { ReactElement } from 'react';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { container } from 'tsyringe';\n\nimport { LoculusService } from '../../../classes/services/LoculusService';\nimport { UrlService } from '../../../classes/services/UrlService';\nimport type { MetadataParseResult } from '../../../models/metadata.models';\nimport { FastaUtil } from '../../../utils/FastaUtil';\n\nexport interface UploadToLoculusModalOpenProps {\n fasta: string;\n metaData: MetadataParseResult | null;\n rawFasta: string;\n}\n\nexport interface UploadToLoculusModalProps extends WithDialogRenderProps<UploadToLoculusModalOpenProps> {\n //\n}\n\nexport type UploadToLoculusModalRefMethods = WithDialogRefMethods<UploadToLoculusModalProps, UploadToLoculusModalOpenProps>;\n\nexport const UploadToLoculusModal = withDialog<UploadToLoculusModalProps, UploadToLoculusModalOpenProps>(({\n onActionsChange,\n onClose,\n openProps,\n}: UploadToLoculusModalProps): ReactElement => {\n const loculusService = useMemo(() => container.resolve(LoculusService), []);\n const urlService = useMemo(() => container.resolve(UrlService), []);\n const [isUploading, setIsUploading] = useState(false);\n const [didUpload, setDidUpload] = useState(false);\n const [uploadError, setUploadError] = useState<null | string>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const fasta = openProps?.fasta ?? '';\n const metaData = openProps?.metaData ?? null;\n const rawFasta = openProps?.rawFasta ?? '';\n\n useEffect(() => () => {\n abortControllerRef.current?.abort();\n }, []);\n\n const validation = useMemo(() => {\n const errors: string[] = [];\n if (!loculusService.data.authenticated) {\n errors.push('You must be authenticated to upload to Loculus.');\n }\n if (!urlService.hasReturnParams()) {\n errors.push('The Loculus return URL and organism must be provided.');\n }\n if (!metaData) {\n errors.push('Metadata must be loaded before uploading.');\n }\n\n const parsedFasta = FastaUtil.parseFasta(fasta);\n if (parsedFasta.error) {\n errors.push(parsedFasta.error);\n } else if (parsedFasta.sequences.length === 0) {\n errors.push('At least one FASTA sequence is required.');\n }\n\n if (metaData?.error) {\n errors.push(metaData.error);\n }\n\n if (errors.length > 0 || !metaData) {\n return { errors, revisedSequences: null };\n }\n\n try {\n const revisedSequences = LoculusService.createRevisedSequences(fasta, metaData, rawFasta);\n if (revisedSequences.sequenceCount === 0) {\n errors.push('No FASTA sequences match the metadata.');\n }\n return {\n errors,\n revisedSequences,\n };\n } catch (error: unknown) {\n errors.push(error instanceof Error ? error.message : 'Failed to prepare the upload preview.');\n return { errors, revisedSequences: null };\n }\n }, [fasta, loculusService, metaData, rawFasta, urlService]);\n const previewMetadataRows = validation.revisedSequences?.metadata\n .split('\\n')\n .slice(1)\n .filter((row) => row.length > 0)\n .reduce<{ key: string; values: string[] }[]>((rows, row) => {\n const occurrence = rows.filter((existingRow) => existingRow.key.startsWith(`${row}-`)).length;\n rows.push({ key: `${row}-${occurrence}`, values: row.split('\\t') });\n return rows;\n }, []);\n\n const onClickUpload = useCallback(async () => {\n if (validation.errors.length > 0 || !validation.revisedSequences || !metaData || !urlService.hasReturnParams()) {\n return;\n }\n\n setUploadError(null);\n setIsUploading(true);\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n try {\n const { organism, returnUrl } = urlService.data;\n await loculusService.reviseSequences(fasta, metaData, returnUrl, organism, abortController);\n setDidUpload(true);\n } catch (error: unknown) {\n if (!abortController.signal.aborted) {\n setUploadError(error instanceof Error ? error.message : 'Failed to upload sequences to Loculus.');\n }\n } finally {\n if (!abortController.signal.aborted) {\n abortControllerRef.current = null;\n setIsUploading(false);\n }\n }\n }, [fasta, loculusService, metaData, urlService, validation.errors.length, validation.revisedSequences]);\n\n const onCancelUpload = useCallback(() => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n setIsUploading(false);\n onClose?.();\n }, [onClose]);\n\n useEffect(() => {\n const actions: DialogAction[] = didUpload\n ? [{\n label: 'Close tab',\n onClick: () => window.close(),\n variant: 'contained',\n }]\n : [\n ...(isUploading ? [{\n label: 'Cancel',\n onClick: onCancelUpload,\n variant: 'outlined' as const,\n }] : []),\n {\n disabled: isUploading || validation.errors.length > 0 || validation.revisedSequences === null,\n label: uploadError ? 'Try again' : 'Upload',\n onClick: onClickUpload,\n variant: 'contained',\n },\n ];\n onActionsChange?.(actions);\n }, [didUpload, isUploading, onActionsChange, onCancelUpload, onClickUpload, uploadError, validation.errors.length, validation.revisedSequences]);\n\n return (\n <Box sx={{ display: 'grid', gap: 2, position: 'relative' }}>\n {isUploading ? (\n <Spinner\n inline\n label={'Uploading sequences to Loculus'}\n />\n ) : (\n <>\n {validation.errors.length > 0 && (\n <Alert severity={'error'}>\n {validation.errors.join(' ')}\n </Alert>\n )}\n {uploadError && (\n <Alert severity={'error'}>\n {uploadError}\n </Alert>\n )}\n {didUpload && (\n <Alert severity={'success'}>\n {'Sequences successfully uploaded to Loculus. You can close this tab.'}\n </Alert>\n )}\n {!didUpload && validation.revisedSequences && (\n <>\n <Alert severity={'info'}>\n {`${validation.revisedSequences.sequenceCount} sequence(s) and the matching metadata will be uploaded to Loculus.`}\n </Alert>\n {validation.revisedSequences.removedSequences.length > 0 && (\n <Alert severity={'warning'}>\n {`Sequences removed during editing and therefore not uploaded: ${validation.revisedSequences.removedSequences.length}`}\n </Alert>\n )}\n {validation.revisedSequences.addedSequencesWithoutMetadata.length > 0 && (\n <Alert severity={'warning'}>\n {`Added sequences without matching metadata will not be uploaded: ${validation.revisedSequences.addedSequencesWithoutMetadata.length}`}\n </Alert>\n )}\n <TableContainer\n component={Paper}\n sx={{ maxHeight: '400px', overflowX: 'auto' }}\n >\n <Table\n size={'small'}\n stickyHeader\n sx={{ minWidth: '100%', whiteSpace: 'nowrap', width: 'max-content' }}\n >\n <TableHead>\n <TableRow>\n {metaData?.columns.map((column) => (\n <TableCell key={column}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {previewMetadataRows?.map(({ key, values }) => (\n <TableRow key={key}>\n {values.map((value, columnIndex) => (\n <TableCell key={`${key}-${metaData?.columns[columnIndex] ?? columnIndex}`}>\n {value}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n </>\n )}\n </>\n )}\n </Box>\n );\n}, {\n defaultTitle: 'Upload to Loculus',\n fullWidth: true,\n maxWidth: 'md',\n testId: 'UploadToLoculusModal',\n});\n"],"mappings":"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"}
|
|
@@ -38,12 +38,13 @@ var f = a(e, {
|
|
|
38
38
|
horizontal: "right",
|
|
39
39
|
vertical: "top"
|
|
40
40
|
},
|
|
41
|
-
children: m.map(({
|
|
41
|
+
children: m.map(({ disabled: e, icon: r, label: a, onClick: o }) => /* @__PURE__ */ l(i, {
|
|
42
|
+
disabled: e,
|
|
42
43
|
onClick: () => {
|
|
43
|
-
x(),
|
|
44
|
+
x(), o();
|
|
44
45
|
},
|
|
45
|
-
children: [/* @__PURE__ */ c(t, { children:
|
|
46
|
-
},
|
|
46
|
+
children: [/* @__PURE__ */ c(t, { children: r }), /* @__PURE__ */ c(n, { primary: a })]
|
|
47
|
+
}, a))
|
|
47
48
|
})] });
|
|
48
49
|
};
|
|
49
50
|
export { p as ButtonWithMenu };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_Fragment","ButtonWithMenuButton","styled","Button","ButtonWithMenu","useThemeProps","inProps","anchorElement","setAnchorElement","useState","isMenuOpen","onClickMenu","useCallback","event","onCloseMenu","Fragment","buttonColor","label","Menu","items","itemLabel","MenuItem","onClick","ListItemIcon","icon","ListItemText"],"sources":["../../../../src/components/ui/ButtonWithMenu/ButtonWithMenu.tsx"],"sourcesContent":["import {\n Button,\n ListItemIcon,\n ListItemText,\n Menu,\n MenuItem,\n styled,\n useThemeProps,\n} from '@mui/material';\nimport type { ButtonOwnProps } from '@mui/material';\nimport type {\n ComponentsOverrides,\n Theme,\n} from '@mui/material/styles';\nimport {\n useCallback,\n useState,\n} from 'react';\nimport type {\n MouseEvent,\n ReactElement,\n} from 'react';\n\nexport type ButtonWithMenuProps = {\n buttonColor?: ButtonOwnProps['color'];\n disabled?: boolean;\n items: Array<{\n icon: ReactElement;\n label: string;\n onClick: () => void;\n }>;\n label: string;\n startIcon?: ReactElement;\n};\n\nexport type ButtonWithMenuTheme = {\n styleOverrides?: ComponentsOverrides<Theme>['MuiButton'];\n};\n\nconst ButtonWithMenuButton = styled(Button, {\n name: 'RivmLoculusToolsCommonFrontendButtonWithMenu',\n slot: 'Root',\n})({\n '&:hover': {\n backgroundColor: 'rgba(255, 255, 255, 0.25)',\n },\n backgroundColor: 'rgba(255, 255, 255, 0.15)',\n color: '#FFFFFF',\n});\n\nexport const ButtonWithMenu = (inProps: ButtonWithMenuProps) => {\n const {\n buttonColor = 'inherit',\n disabled,\n items,\n label,\n startIcon,\n } = useThemeProps({ name: 'RivmLoculusToolsCommonFrontendButtonWithMenu', props: inProps });\n const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null);\n const isMenuOpen = Boolean(anchorElement);\n\n const onClickMenu = useCallback((event: MouseEvent<HTMLButtonElement>) => {\n setAnchorElement(event.currentTarget);\n }, []);\n\n const onCloseMenu = useCallback(() => {\n setAnchorElement(null);\n }, []);\n\n return (\n <>\n <ButtonWithMenuButton\n color={buttonColor}\n disabled={disabled}\n onClick={onClickMenu}\n size={'small'}\n startIcon={startIcon}\n variant={'contained'}\n >\n {label}\n </ButtonWithMenuButton>\n <Menu\n anchorEl={anchorElement}\n anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}\n onClose={onCloseMenu}\n open={isMenuOpen}\n transformOrigin={{ horizontal: 'right', vertical: 'top' }}\n >\n {items.map(({ icon, label: itemLabel, onClick }) => (\n <MenuItem\n key={itemLabel}\n // eslint-disable-next-line @eslint-react/kit/jsx-no-bind\n onClick={() => {\n onCloseMenu();\n onClick();\n }}\n >\n <ListItemIcon>\n {icon}\n </ListItemIcon>\n <ListItemText primary={itemLabel} />\n </MenuItem>\n ))}\n </Menu>\n </>\n );\n};\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment","ButtonWithMenuButton","styled","Button","ButtonWithMenu","useThemeProps","inProps","anchorElement","setAnchorElement","useState","isMenuOpen","onClickMenu","useCallback","event","onCloseMenu","Fragment","buttonColor","label","Menu","items","itemDisabled","itemLabel","MenuItem","onClick","ListItemIcon","icon","ListItemText"],"sources":["../../../../src/components/ui/ButtonWithMenu/ButtonWithMenu.tsx"],"sourcesContent":["import {\n Button,\n ListItemIcon,\n ListItemText,\n Menu,\n MenuItem,\n styled,\n useThemeProps,\n} from '@mui/material';\nimport type { ButtonOwnProps } from '@mui/material';\nimport type {\n ComponentsOverrides,\n Theme,\n} from '@mui/material/styles';\nimport {\n useCallback,\n useState,\n} from 'react';\nimport type {\n MouseEvent,\n ReactElement,\n} from 'react';\n\nexport type ButtonWithMenuProps = {\n buttonColor?: ButtonOwnProps['color'];\n disabled?: boolean;\n items: Array<{\n disabled?: boolean;\n icon: ReactElement;\n label: string;\n onClick: () => void;\n }>;\n label: string;\n startIcon?: ReactElement;\n};\n\nexport type ButtonWithMenuTheme = {\n styleOverrides?: ComponentsOverrides<Theme>['MuiButton'];\n};\n\nconst ButtonWithMenuButton = styled(Button, {\n name: 'RivmLoculusToolsCommonFrontendButtonWithMenu',\n slot: 'Root',\n})({\n '&:hover': {\n backgroundColor: 'rgba(255, 255, 255, 0.25)',\n },\n backgroundColor: 'rgba(255, 255, 255, 0.15)',\n color: '#FFFFFF',\n});\n\nexport const ButtonWithMenu = (inProps: ButtonWithMenuProps) => {\n const {\n buttonColor = 'inherit',\n disabled,\n items,\n label,\n startIcon,\n } = useThemeProps({ name: 'RivmLoculusToolsCommonFrontendButtonWithMenu', props: inProps });\n const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null);\n const isMenuOpen = Boolean(anchorElement);\n\n const onClickMenu = useCallback((event: MouseEvent<HTMLButtonElement>) => {\n setAnchorElement(event.currentTarget);\n }, []);\n\n const onCloseMenu = useCallback(() => {\n setAnchorElement(null);\n }, []);\n\n return (\n <>\n <ButtonWithMenuButton\n color={buttonColor}\n disabled={disabled}\n onClick={onClickMenu}\n size={'small'}\n startIcon={startIcon}\n variant={'contained'}\n >\n {label}\n </ButtonWithMenuButton>\n <Menu\n anchorEl={anchorElement}\n anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}\n onClose={onCloseMenu}\n open={isMenuOpen}\n transformOrigin={{ horizontal: 'right', vertical: 'top' }}\n >\n {items.map(({ disabled: itemDisabled, icon, label: itemLabel, onClick }) => (\n <MenuItem\n disabled={itemDisabled}\n key={itemLabel}\n // eslint-disable-next-line @eslint-react/kit/jsx-no-bind\n onClick={() => {\n onCloseMenu();\n onClick();\n }}\n >\n <ListItemIcon>\n {icon}\n </ListItemIcon>\n <ListItemText primary={itemLabel} />\n </MenuItem>\n ))}\n </Menu>\n </>\n );\n};\n"],"mappings":";;;;AAwCA,IAAMC,IAAuBC,EAAOC,GAAQ;CAC1C,MAAM;CACN,MAAM;AACR,CAAC,CAAC,CAAC;CACD,WAAW,EACT,iBAAiB,4BACnB;CACA,iBAAiB;CACjB,OAAO;AACT,CAAC,GAEYC,KAAkB,MAAiC;CAC9D,IAAM,EACJ,iBAAc,WACd,aACA,UACA,UACA,iBACEC,EAAc;EAAE,MAAM;EAAgD,OAAOC;CAAQ,CAAC,GACpF,CAACC,GAAeC,KAAoBC,EAA6B,IAAI,GACrEC,IAAa,EAAQH,GAErBI,IAAcC,GAAa,MAAyC;EACxE,EAAiBC,EAAM,aAAa;CACtC,GAAG,CAAC,CAAC,GAECC,IAAcF,QAAkB;EACpC,EAAiB,IAAI;CACvB,GAAG,CAAC,CAAC;CAEL,OACE,kBAAAZ,GAAA,EAAA,UAAA,CACE,kBAACC,GAAD;EACE,OAAOe;EACG;EACV,SAASL;EACT,MAAM;EACK;EACX,SAAS;EAER,UAAAM;CACmB,CAAA,GACtB,kBAACC,GAAD;EACE,UAAUX;EACV,cAAc;GAAE,YAAY;GAAS,UAAU;EAAS;EACxD,SAASO;EACT,MAAMJ;EACN,iBAAiB;GAAE,YAAY;GAAS,UAAU;EAAM;EAEvD,UAAAS,EAAM,KAAK,EAAE,UAAUC,GAAc,SAAM,OAAOC,GAAW,iBAC5D,kBAACC,GAAD;GACE,UAAUF;GAGV,eAAe;IAEb,AADAN,EAAY,GACZS,EAAQ;GACV;GAPF,UAAA,CASE,kBAACC,GAAD,EAAA,UACGC,EACW,CAAA,GACd,kBAACC,GAAD,EAAc,SAASL,EAAY,CAAA,CAC3B;EAXH,GAAAA,CAWG,CACX;CACG,CAAA,CACN,EAAA,CAAA;AAEN"}
|
|
@@ -15,7 +15,7 @@ var l = ({ children: l, isNoInteraction: u, title: d }) => {
|
|
|
15
15
|
}, [u, _]), /* @__PURE__ */ o(r, {
|
|
16
16
|
sx: {
|
|
17
17
|
display: "grid",
|
|
18
|
-
gridTemplateRows: "max-content auto",
|
|
18
|
+
gridTemplateRows: p ? "max-content auto" : "auto",
|
|
19
19
|
height: "100vh",
|
|
20
20
|
overflow: "hidden",
|
|
21
21
|
position: "relative",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["PageCommon","DrawerComponent","HeaderComponent","useConfig","title","theme","useTheme","use","PageCommonContext","useEffect","isNoInteraction","setIsNoInteraction","Box","drawerContainerId","isDrawerOpen"],"sources":["../../../../src/components/ui/PageCommon/PageCommon.tsx"],"sourcesContent":["import {\n Box,\n useTheme,\n} from '@mui/material';\nimport {\n use,\n useEffect,\n} from 'react';\nimport type { ReactNode } from 'react';\n\nimport { DRAWER_WIDTH } from '../../../constants/page.constants';\nimport { PageCommonContext } from '../../../context/pageCommonContext';\nimport { useUpdateDocumentTitle } from '../../../hooks/useUpdateDocumentTitle';\nimport { useConfig } from '../../../hooks/useConfig';\n\nexport type PageCommonProps = {\n readonly children: ReactNode;\n readonly isNoInteraction?: boolean;\n readonly title: string;\n};\n\n\nexport const PageCommon = ({ children, isNoInteraction, title }: PageCommonProps) => {\n const { drawerComponent: DrawerComponent, headerComponent: HeaderComponent } = useConfig();\n\n useUpdateDocumentTitle(title);\n const theme = useTheme();\n const { drawerContainerId, isDrawerOpen, setIsNoInteraction } = use(PageCommonContext);\n\n useEffect(() => {\n setIsNoInteraction(!!isNoInteraction);\n }, [isNoInteraction, setIsNoInteraction]);\n\n return (\n <Box\n sx={{\n display: 'grid',\n gridTemplateRows: 'max-content auto',\n height: '100vh',\n overflow: 'hidden',\n position: 'relative',\n width: '100vw',\n }}\n >\n {HeaderComponent && <HeaderComponent />}\n <Box\n id={drawerContainerId}\n sx={{\n display: 'flex',\n flex: 1,\n height: '100%',\n overflow: 'hidden',\n position: 'relative',\n width: '100%',\n }}\n >\n {DrawerComponent && <DrawerComponent />}\n <Box\n component={'main'}\n sx={{\n flexGrow: 1,\n height: '100%',\n overflow: 'hidden',\n position: 'relative',\n transition: () =>\n theme.transitions.create(['margin', 'width'], {\n duration: theme.transitions.duration.leavingScreen,\n easing: theme.transitions.easing.sharp,\n }),\n width: isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%',\n }}\n >\n {children}\n </Box>\n\n </Box>\n </Box>\n );\n};\n"],"mappings":";;;;;;;;AAsBA,IAAaA,KAAc,EAAE,aAAU,oBAAiB,eAA6B;CACnF,IAAM,EAAE,iBAAiBC,GAAiB,iBAAiBC,MAAoBC,EAAU;CAEzF,EAAuBC,CAAK;CAC5B,IAAMC,IAAQC,EAAS,GACjB,EAAE,sBAAmB,iBAAc,0BAAuBC,EAAIC,CAAiB;CAMrF,OAJAC,QAAgB;EACd,EAAmB,CAAC,CAACC,CAAe;CACtC,GAAG,CAACA,GAAiBC,CAAkB,CAAC,GAGtC,kBAACC,GAAD;EACE,IAAI;GACF,SAAS;GACT,
|
|
1
|
+
{"version":3,"file":"index.js","names":["PageCommon","DrawerComponent","HeaderComponent","useConfig","title","theme","useTheme","use","PageCommonContext","useEffect","isNoInteraction","setIsNoInteraction","Box","drawerContainerId","isDrawerOpen"],"sources":["../../../../src/components/ui/PageCommon/PageCommon.tsx"],"sourcesContent":["import {\n Box,\n useTheme,\n} from '@mui/material';\nimport {\n use,\n useEffect,\n} from 'react';\nimport type { ReactNode } from 'react';\n\nimport { DRAWER_WIDTH } from '../../../constants/page.constants';\nimport { PageCommonContext } from '../../../context/pageCommonContext';\nimport { useUpdateDocumentTitle } from '../../../hooks/useUpdateDocumentTitle';\nimport { useConfig } from '../../../hooks/useConfig';\n\nexport type PageCommonProps = {\n readonly children: ReactNode;\n readonly isNoInteraction?: boolean;\n readonly title: string;\n};\n\n\nexport const PageCommon = ({ children, isNoInteraction, title }: PageCommonProps) => {\n const { drawerComponent: DrawerComponent, headerComponent: HeaderComponent } = useConfig();\n\n useUpdateDocumentTitle(title);\n const theme = useTheme();\n const { drawerContainerId, isDrawerOpen, setIsNoInteraction } = use(PageCommonContext);\n\n useEffect(() => {\n setIsNoInteraction(!!isNoInteraction);\n }, [isNoInteraction, setIsNoInteraction]);\n\n return (\n <Box\n sx={{\n display: 'grid',\n gridTemplateRows: HeaderComponent ? 'max-content auto' : 'auto',\n height: '100vh',\n overflow: 'hidden',\n position: 'relative',\n width: '100vw',\n }}\n >\n {HeaderComponent && <HeaderComponent />}\n <Box\n id={drawerContainerId}\n sx={{\n display: 'flex',\n flex: 1,\n height: '100%',\n overflow: 'hidden',\n position: 'relative',\n width: '100%',\n }}\n >\n {DrawerComponent && <DrawerComponent />}\n <Box\n component={'main'}\n sx={{\n flexGrow: 1,\n height: '100%',\n overflow: 'hidden',\n position: 'relative',\n transition: () =>\n theme.transitions.create(['margin', 'width'], {\n duration: theme.transitions.duration.leavingScreen,\n easing: theme.transitions.easing.sharp,\n }),\n width: isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%',\n }}\n >\n {children}\n </Box>\n\n </Box>\n </Box>\n );\n};\n"],"mappings":";;;;;;;;AAsBA,IAAaA,KAAc,EAAE,aAAU,oBAAiB,eAA6B;CACnF,IAAM,EAAE,iBAAiBC,GAAiB,iBAAiBC,MAAoBC,EAAU;CAEzF,EAAuBC,CAAK;CAC5B,IAAMC,IAAQC,EAAS,GACjB,EAAE,sBAAmB,iBAAc,0BAAuBC,EAAIC,CAAiB;CAMrF,OAJAC,QAAgB;EACd,EAAmB,CAAC,CAACC,CAAe;CACtC,GAAG,CAACA,GAAiBC,CAAkB,CAAC,GAGtC,kBAACC,GAAD;EACE,IAAI;GACF,SAAS;GACT,kBAAkBV,IAAkB,qBAAqB;GACzD,QAAQ;GACR,UAAU;GACV,UAAU;GACV,OAAO;EACT;EARF,UAAA,CAUGA,KAAmB,kBAACA,GAAD,CAAkB,CAAA,GACtC,kBAACU,GAAD;GACE,IAAIC;GACJ,IAAI;IACF,SAAS;IACT,MAAM;IACN,QAAQ;IACR,UAAU;IACV,UAAU;IACV,OAAO;GACT;GATF,UAAA,CAWGZ,KAAmB,kBAACA,GAAD,CAAkB,CAAA,GACtC,kBAACW,GAAD;IACE,WAAW;IACX,IAAI;KACF,UAAU;KACV,QAAQ;KACR,UAAU;KACV,UAAU;KACV,kBACEP,EAAM,YAAY,OAAO,CAAC,UAAU,OAAO,GAAG;MAC5C,UAAUA,EAAM,YAAY,SAAS;MACrC,QAAQA,EAAM,YAAY,OAAO;KACnC,CAAC;KACH,OAAOS,IAAe,uBAAmC;IAC3D;IAEC;GACE,CAAA,CAEF;EACF,CAAA,CAAA;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_Fragment","SearchIcon","KeyboardArrowUpIcon","KeyboardArrowDownIcon","CloseIcon","SearchBarFilledInput","styled","FilledInput","theme","SearchBar","useThemeProps","inProps","useTheme","searchBarTheme","useMemo","inputRef","useRef","arrowUpButtonRef","arrowDownButtonRef","onTextInputChange","useCallback","nextSearchText","event","onSearchQueryChange","onClearButtonClick","onTextInputKeyDown","numResults","onHighlightedIndexChange","onUpArrowButtonClick","newHighlightedIndex","highlightedIndex","onDownArrowButtonClick","onGenericKeyDown","nextFocusableElement","previousFocusableElement","searchResultStatusText","searchQuery","allowNavigation","truncated","InputAdornment","Box","Fragment","IconButton"],"sources":["../../../../src/components/ui/SearchBar/SearchBar.tsx"],"sourcesContent":["import type {\n ChangeEvent,\n KeyboardEvent,\n} from 'react';\nimport {\n useCallback,\n useMemo,\n useRef,\n} from 'react';\nimport {\n Box,\n FilledInput,\n IconButton,\n InputAdornment,\n styled,\n useTheme,\n useThemeProps,\n} from '@mui/material';\nimport type {\n ComponentsOverrides,\n Theme,\n} from '@mui/material/styles';\nimport SearchIcon from '@mui/icons-material/Search';\nimport KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';\nimport KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';\nimport CloseIcon from '@mui/icons-material/Close';\n\nexport type SearchBarProps = {\n allowNavigation?: boolean;\n highlightedIndex?: null | number;\n numResults?: number;\n onHighlightedIndexChange?: (index: number) => void;\n onSearchQueryChange: (query: string) => void;\n searchQuery: string;\n truncated?: boolean;\n};\n\nexport interface SearchBarTheme {\n buttonColor: string;\n styleOverrides?: ComponentsOverrides<Theme>['MuiFilledInput'];\n}\n\nconst SearchBarFilledInput = styled(FilledInput, {\n name: 'RivmLoculusToolsCommonFrontendSearchBar',\n slot: 'Root',\n})(({ theme }) => ({\n backgroundColor: theme.palette.background.paper,\n height: theme.spacing(4),\n}));\n\nexport const SearchBar = (inProps: SearchBarProps) => {\n const {\n allowNavigation,\n highlightedIndex,\n numResults = 0,\n onHighlightedIndexChange,\n onSearchQueryChange,\n searchQuery,\n truncated,\n } = useThemeProps({ name: 'RivmLoculusToolsCommonFrontendSearchBar', props: inProps });\n const theme = useTheme();\n const searchBarTheme = useMemo<SearchBarTheme>(() => ({\n ...{\n buttonColor: theme.palette.primary.main,\n } satisfies SearchBarTheme,\n ...(theme.components?.RivmLoculusToolsCommonFrontendSearchBar ?? {}),\n }), [theme]);\n\n\n const inputRef = useRef<HTMLInputElement>(null);\n const arrowUpButtonRef = useRef<HTMLButtonElement>(null);\n const arrowDownButtonRef = useRef<HTMLButtonElement>(null);\n\n const onTextInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {\n const nextSearchText = event.target.value;\n onSearchQueryChange(nextSearchText);\n }, [onSearchQueryChange]);\n\n const onClearButtonClick = useCallback(() => {\n onSearchQueryChange('');\n inputRef.current?.focus();\n }, [onSearchQueryChange]);\n\n const onTextInputKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {\n // on enter press, highlight the first result and focus the down arrow button\n if (event.key === 'Enter') {\n if (!numResults) {\n return;\n }\n event.preventDefault();\n onHighlightedIndexChange?.(0);\n arrowDownButtonRef.current?.focus();\n }\n }, [numResults, onHighlightedIndexChange]);\n\n const onUpArrowButtonClick = useCallback(() => {\n // goto previous result\n if (!numResults) {\n return;\n }\n const newHighlightedIndex = highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex <= 0 ? numResults - 1 : highlightedIndex - 1;\n\n onHighlightedIndexChange?.(newHighlightedIndex);\n }, [numResults, highlightedIndex, onHighlightedIndexChange]);\n\n const onDownArrowButtonClick = useCallback(() => {\n // goto next result\n if (!numResults) {\n return;\n }\n const newHighlightedIndex = highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex >= numResults - 1 ? 0 : highlightedIndex + 1;\n onHighlightedIndexChange?.(newHighlightedIndex);\n }, [numResults, highlightedIndex, onHighlightedIndexChange]);\n\n const onGenericKeyDown = useCallback((event: KeyboardEvent<HTMLButtonElement>) => {\n // arrow right, focus the next focusable element\n if (event.key === 'ArrowRight') {\n event.preventDefault();\n const nextFocusableElement = event.currentTarget.nextElementSibling as HTMLElement;\n if (nextFocusableElement) {\n nextFocusableElement.focus();\n }\n }\n // arrow left, focus the previous focusable element\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n if (event.target === arrowUpButtonRef.current) {\n inputRef.current?.focus();\n return;\n }\n const previousFocusableElement = event.currentTarget.previousElementSibling as HTMLElement;\n if (previousFocusableElement) {\n previousFocusableElement.focus();\n }\n }\n }, []);\n\n const searchResultStatusText = useMemo(() => {\n if (!searchQuery) {\n return '';\n }\n if (searchQuery && (Number.isNaN(numResults) || numResults <= 0)) {\n return 'No results';\n } else if (highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex < 0 || !allowNavigation) {\n return `${numResults}${truncated ? '+' : ''} results`;\n } else if (allowNavigation) {\n return `${highlightedIndex + 1}/${numResults}${truncated ? '+' : ''} results`;\n }\n return '';\n }, [searchQuery, numResults, highlightedIndex, allowNavigation, truncated]);\n\n\n return (\n <SearchBarFilledInput\n endAdornment={(\n <InputAdornment position={'end'}>\n {searchResultStatusText && (\n <Box\n aria-atomic={'true'}\n aria-live={'polite'}\n role={'status'}\n sx={{\n fontSize: '1rem',\n }}\n >\n {searchResultStatusText}\n </Box>\n )}\n {numResults > 0 && allowNavigation && (\n <>\n <IconButton\n aria-label={'Goto previous search result'}\n disabled={!numResults}\n edge={'end'}\n onClick={onUpArrowButtonClick}\n onKeyDown={onGenericKeyDown}\n ref={arrowUpButtonRef}\n sx={{ color: searchBarTheme.buttonColor }}\n >\n <KeyboardArrowUpIcon />\n </IconButton>\n <IconButton\n aria-label={'Goto next search result'}\n disabled={!numResults}\n edge={'end'}\n onClick={onDownArrowButtonClick}\n onKeyDown={onGenericKeyDown}\n ref={arrowDownButtonRef}\n sx={{ color: searchBarTheme.buttonColor }}\n >\n <KeyboardArrowDownIcon />\n </IconButton>\n </>\n )}\n {searchQuery && (\n <IconButton\n aria-label={'Clear'}\n edge={'end'}\n onClick={onClearButtonClick}\n onKeyDown={onGenericKeyDown}\n >\n <CloseIcon />\n </IconButton>\n )}\n </InputAdornment>\n )}\n fullWidth\n inputRef={inputRef}\n name={'Search'}\n onChange={onTextInputChange}\n onKeyDown={onTextInputKeyDown}\n placeholder={'Search'}\n size={'small'}\n slotProps={{\n input: {\n 'aria-label': 'Search',\n sx: {\n '&::-webkit-search-cancel-button': {\n display: 'none',\n },\n fontSize: '1rem',\n height: theme.spacing(4),\n outline: 'none !important',\n paddingBottom: '0 !important',\n paddingTop: '0 !important',\n },\n },\n }}\n startAdornment={(\n <InputAdornment position={'start'}>\n <SearchIcon fontSize={'small'} />\n </InputAdornment>\n )}\n type={'search'}\n value={searchQuery}\n />\n );\n};\n"],"mappings":";;;;;;;;AA0CA,IAAMK,IAAuBC,EAAOC,GAAa;CAC/C,MAAM;CACN,MAAM;AACR,CAAC,CAAC,EAAE,EAAE,gBAAa;CACjB,iBAAiBC,EAAM,QAAQ,WAAW;CAC1C,QAAQA,EAAM,QAAQ,CAAC;AACzB,EAAE,GAEWC,KAAa,MAA4B;CACpD,IAAM,EACJ,oBACA,qBACA,gBAAa,GACb,6BACA,wBACA,gBACA,iBACEC,EAAc;EAAE,MAAM;EAA2C,OAAOC;CAAQ,CAAC,GAC/EH,IAAQI,EAAS,GACjBC,IAAiBC,SAA+B;EAElD,aAAaN,EAAM,QAAQ,QAAQ;EAErC,GAAIA,EAAM,YAAY,2CAA2C,CAAC;CACpE,IAAI,CAACA,CAAK,CAAC,GAGLO,IAAWC,EAAyB,IAAI,GACxCC,IAAmBD,EAA0B,IAAI,GACjDE,IAAqBF,EAA0B,IAAI,GAEnDG,IAAoBC,GAAa,MAAyC;EAC9E,IAAMC,IAAiBC,EAAM,OAAO;EACpC,EAAoBD,CAAc;CACpC,GAAG,CAACE,CAAmB,CAAC,GAElBC,IAAqBJ,QAAkB;EAE3C,AADAG,EAAoB,EAAE,GACtBR,EAAS,SAAS,MAAM;CAC1B,GAAG,CAACQ,CAAmB,CAAC,GAElBE,IAAqBL,GAAa,MAA2C;EAEjF,IAAIE,EAAM,QAAQ,SAAS;GACzB,IAAI,CAACI,GACH;GAIF,AAFAJ,EAAM,eAAe,GACrBK,IAA2B,CAAC,GAC5BT,EAAmB,SAAS,MAAM;EACpC;CACF,GAAG,CAACQ,GAAYC,CAAwB,CAAC,GAEnCC,IAAuBR,QAAkB;EAE7C,IAAI,CAACM,GACH;EAEF,IAAMG,IAAsBC,KAAqB,QAA0C,OAAO,MAAMA,CAAgB,KAAKA,KAAoB,IAAIJ,IAAa,IAAII,IAAmB;EAEzL,IAA2BD,CAAmB;CAChD,GAAG;EAACH;EAAYI;EAAkBH;CAAwB,CAAC,GAErDI,IAAyBX,QAAkB;EAE/C,IAAI,CAACM,GACH;EAEF,IAAMG,IAAsBC,KAAqB,QAA0C,OAAO,MAAMA,CAAgB,KAAKA,KAAoBJ,IAAa,IAAI,IAAII,IAAmB;EACzL,IAA2BD,CAAmB;CAChD,GAAG;EAACH;EAAYI;EAAkBH;CAAwB,CAAC,GAErDK,IAAmBZ,GAAa,MAA4C;EAEhF,IAAIE,EAAM,QAAQ,cAAc;GAC9B,EAAM,eAAe;GACrB,IAAMW,IAAuBX,EAAM,cAAc;GACjD,AAAIW,KACFA,EAAqB,MAAM;EAE/B;EAEA,IAAIX,EAAM,QAAQ,aAAa;GAE7B,IADAA,EAAM,eAAe,GACjBA,EAAM,WAAWL,EAAiB,SAAS;IAC7C,EAAS,SAAS,MAAM;IACxB;GACF;GACA,IAAMiB,IAA2BZ,EAAM,cAAc;GACrD,AAAIY,KACFA,EAAyB,MAAM;EAEnC;CACF,GAAG,CAAC,CAAC,GAECC,IAAyBrB,QACxBsB,IAGDA,MAAgB,OAAO,MAAMV,CAAU,KAAKA,KAAc,KACrD,eACEI,KAAqB,QAA0C,OAAO,MAAMA,CAAgB,KAAKA,IAAmB,KAAK,CAACO,IAC5H,GAAGX,IAAaY,IAAY,MAAM,GAAG,YACnCD,IACF,GAAGP,IAAmB,EAAE,GAAGJ,IAAaY,IAAY,MAAM,GAAG,YAE/D,KATE,IAUR;EAACF;EAAaV;EAAYI;EAAkBO;EAAiBC;CAAS,CAAC;CAG1E,OACE,kBAACjC,GAAD;EACE,cACE,kBAACkC,GAAD;GAAgB,UAAU;GAA1B,UAAA;IACGJ,KACC,kBAACK,GAAD;KACE,eAAa;KACb,aAAW;KACX,MAAM;KACN,IAAI,EACF,UAAU,OACZ;KAEC,UAAAL;IACE,CAAA;IAENT,IAAa,KAAKW,KACjB,kBAAArC,GAAA,EAAA,UAAA,CACE,kBAAC0C,GAAD;KACE,cAAY;KACZ,UAAU,CAAChB;KACX,MAAM;KACN,SAASE;KACT,WAAWI;KACX,KAAKf;KACL,IAAI,EAAE,OAAOJ,EAAe,YAAY;KAExC,UAAA,kBAACX,GAAD,CAAsB,CAAA;IACZ,CAAA,GACZ,kBAACwC,GAAD;KACE,cAAY;KACZ,UAAU,CAAChB;KACX,MAAM;KACN,SAASK;KACT,WAAWC;KACX,KAAKd;KACL,IAAI,EAAE,OAAOL,EAAe,YAAY;KAExC,UAAA,kBAACV,GAAD,CAAwB,CAAA;IACd,CAAA,CACZ,EAAA,CAAA;IAEHiC,KACC,kBAACM,GAAD;KACE,cAAY;KACZ,MAAM;KACN,SAASlB;KACT,WAAWQ;KAEX,UAAA,kBAAC5B,GAAD,CAAY,CAAA;IACF,CAAA;GAEA;;EAElB,WAAA;EACU;EACV,MAAM;EACN,UAAUe;EACV,WAAWM;EACX,aAAa;EACb,MAAM;EACN,WAAW,EACT,OAAO;GACL,cAAc;GACd,IAAI;IACF,mCAAmC,EACjC,SAAS,OACX;IACA,UAAU;IACV,QAAQjB,EAAM,QAAQ,CAAC;IACvB,SAAS;IACT,eAAe;IACf,YAAY;GACd;EACF,EACF;EACA,gBACE,kBAAC+B,GAAD;GAAgB,UAAU;GACxB,UAAA,kBAACtC,GAAD,EAAY,UAAU,QAAU,CAAA;EAClB,CAAA;EAElB,MAAM;EACN,OAAOmC;CACR,CAAA;AAEL"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment","SearchIcon","KeyboardArrowUpIcon","KeyboardArrowDownIcon","CloseIcon","SearchBarFilledInput","styled","FilledInput","theme","SearchBar","useThemeProps","inProps","useTheme","searchBarTheme","useMemo","inputRef","useRef","arrowUpButtonRef","arrowDownButtonRef","onTextInputChange","useCallback","nextSearchText","event","onSearchQueryChange","onClearButtonClick","onTextInputKeyDown","numResults","onHighlightedIndexChange","onUpArrowButtonClick","newHighlightedIndex","highlightedIndex","onDownArrowButtonClick","onGenericKeyDown","nextFocusableElement","previousFocusableElement","searchResultStatusText","searchQuery","allowNavigation","truncated","InputAdornment","Box","Fragment","IconButton"],"sources":["../../../../src/components/ui/SearchBar/SearchBar.tsx"],"sourcesContent":["import type {\n ChangeEvent,\n KeyboardEvent,\n} from 'react';\nimport {\n useCallback,\n useMemo,\n useRef,\n} from 'react';\nimport {\n Box,\n FilledInput,\n IconButton,\n InputAdornment,\n styled,\n useTheme,\n useThemeProps,\n} from '@mui/material';\nimport type {\n ComponentsOverrides,\n Theme,\n} from '@mui/material/styles';\nimport SearchIcon from '@mui/icons-material/Search';\nimport KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';\nimport KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';\nimport CloseIcon from '@mui/icons-material/Close';\n\nexport type SearchBarProps = {\n allowNavigation?: boolean;\n highlightedIndex?: null | number;\n numResults?: number;\n onHighlightedIndexChange?: (index: number) => void;\n onSearchQueryChange: (query: string) => void;\n searchQuery: string;\n truncated?: boolean;\n};\n\nexport interface SearchBarTheme {\n buttonColor: string;\n styleOverrides?: ComponentsOverrides<Theme>['MuiFilledInput'];\n}\n\nconst SearchBarFilledInput = styled(FilledInput, {\n name: 'RivmLoculusToolsCommonFrontendSearchBar',\n slot: 'Root',\n})(({ theme }) => ({\n backgroundColor: theme.palette.background.paper,\n height: theme.spacing(4),\n}));\n\nexport const SearchBar = (inProps: SearchBarProps) => {\n const {\n allowNavigation,\n highlightedIndex,\n numResults = 0,\n onHighlightedIndexChange,\n onSearchQueryChange,\n searchQuery,\n truncated,\n } = useThemeProps({ name: 'RivmLoculusToolsCommonFrontendSearchBar', props: inProps });\n const theme = useTheme();\n const searchBarTheme = useMemo<SearchBarTheme>(() => ({\n ...{\n buttonColor: theme.palette.primary.main,\n } satisfies SearchBarTheme,\n ...(theme.components?.RivmLoculusToolsCommonFrontendSearchBar ?? {}),\n }), [theme]);\n\n\n const inputRef = useRef<HTMLInputElement>(null);\n const arrowUpButtonRef = useRef<HTMLButtonElement>(null);\n const arrowDownButtonRef = useRef<HTMLButtonElement>(null);\n\n const onTextInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {\n const nextSearchText = event.target.value;\n onSearchQueryChange(nextSearchText);\n }, [onSearchQueryChange]);\n\n const onClearButtonClick = useCallback(() => {\n onSearchQueryChange('');\n inputRef.current?.focus();\n }, [onSearchQueryChange]);\n\n const onTextInputKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {\n // on enter press, highlight the first result and focus the down arrow button\n if (event.key === 'Enter') {\n if (!numResults) {\n return;\n }\n event.preventDefault();\n onHighlightedIndexChange?.(0);\n arrowDownButtonRef.current?.focus();\n }\n }, [numResults, onHighlightedIndexChange]);\n\n const onUpArrowButtonClick = useCallback(() => {\n // goto previous result\n if (!numResults) {\n return;\n }\n const newHighlightedIndex = highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex <= 0 ? numResults - 1 : highlightedIndex - 1;\n\n onHighlightedIndexChange?.(newHighlightedIndex);\n }, [numResults, highlightedIndex, onHighlightedIndexChange]);\n\n const onDownArrowButtonClick = useCallback(() => {\n // goto next result\n if (!numResults) {\n return;\n }\n const newHighlightedIndex = highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex >= numResults - 1 ? 0 : highlightedIndex + 1;\n onHighlightedIndexChange?.(newHighlightedIndex);\n }, [numResults, highlightedIndex, onHighlightedIndexChange]);\n\n const onGenericKeyDown = useCallback((event: KeyboardEvent<HTMLButtonElement>) => {\n // arrow right, focus the next focusable element\n if (event.key === 'ArrowRight') {\n event.preventDefault();\n const nextFocusableElement = event.currentTarget.nextElementSibling as HTMLElement;\n if (nextFocusableElement) {\n nextFocusableElement.focus();\n }\n }\n // arrow left, focus the previous focusable element\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n if (event.target === arrowUpButtonRef.current) {\n inputRef.current?.focus();\n return;\n }\n const previousFocusableElement = event.currentTarget.previousElementSibling as HTMLElement;\n if (previousFocusableElement) {\n previousFocusableElement.focus();\n }\n }\n }, []);\n\n const searchResultStatusText = useMemo(() => {\n if (!searchQuery) {\n return '';\n }\n if (searchQuery && (Number.isNaN(numResults) || numResults <= 0)) {\n return 'No results';\n } else if (highlightedIndex === null || highlightedIndex === undefined || Number.isNaN(highlightedIndex) || highlightedIndex < 0 || !allowNavigation) {\n return `${numResults}${truncated ? '+' : ''} results`;\n } else if (allowNavigation) {\n return `${highlightedIndex + 1}/${numResults}${truncated ? '+' : ''} results`;\n }\n return '';\n }, [searchQuery, numResults, highlightedIndex, allowNavigation, truncated]);\n\n\n return (\n <SearchBarFilledInput\n autoComplete={'off'}\n endAdornment={(\n <InputAdornment position={'end'}>\n {searchResultStatusText && (\n <Box\n aria-atomic={'true'}\n aria-live={'polite'}\n role={'status'}\n sx={{\n fontSize: '1rem',\n }}\n >\n {searchResultStatusText}\n </Box>\n )}\n {numResults > 0 && allowNavigation && (\n <>\n <IconButton\n aria-label={'Goto previous search result'}\n disabled={!numResults}\n edge={'end'}\n onClick={onUpArrowButtonClick}\n onKeyDown={onGenericKeyDown}\n ref={arrowUpButtonRef}\n sx={{ color: searchBarTheme.buttonColor }}\n >\n <KeyboardArrowUpIcon />\n </IconButton>\n <IconButton\n aria-label={'Goto next search result'}\n disabled={!numResults}\n edge={'end'}\n onClick={onDownArrowButtonClick}\n onKeyDown={onGenericKeyDown}\n ref={arrowDownButtonRef}\n sx={{ color: searchBarTheme.buttonColor }}\n >\n <KeyboardArrowDownIcon />\n </IconButton>\n </>\n )}\n {searchQuery && (\n <IconButton\n aria-label={'Clear'}\n edge={'end'}\n onClick={onClearButtonClick}\n onKeyDown={onGenericKeyDown}\n >\n <CloseIcon />\n </IconButton>\n )}\n </InputAdornment>\n )}\n fullWidth\n inputRef={inputRef}\n name={'Search'}\n onChange={onTextInputChange}\n onKeyDown={onTextInputKeyDown}\n placeholder={'Search'}\n size={'small'}\n slotProps={{\n input: {\n 'aria-label': 'Search',\n sx: {\n '&::-webkit-search-cancel-button': {\n display: 'none',\n },\n fontSize: '1rem',\n height: theme.spacing(4),\n outline: 'none !important',\n paddingBottom: '0 !important',\n paddingTop: '0 !important',\n },\n },\n }}\n startAdornment={(\n <InputAdornment position={'start'}>\n <SearchIcon fontSize={'small'} />\n </InputAdornment>\n )}\n type={'search'}\n value={searchQuery}\n />\n );\n};\n"],"mappings":"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"}
|
|
File without changes
|
|
@@ -7,6 +7,7 @@ export declare class MetaDataUtil {
|
|
|
7
7
|
bestColumn: null | string;
|
|
8
8
|
matchCounts: Record<string, number>;
|
|
9
9
|
};
|
|
10
|
+
static getGroupId(metaData: MetadataParseResult): string;
|
|
10
11
|
static getHighlightedIds(query: string, allAccessions: string[], metadataRecords: MetadataRecord[], matchKeyField?: null | string): string[];
|
|
11
12
|
/**
|
|
12
13
|
* Count FASTA sequence ID matches for each metadata column.
|
|
@@ -16,5 +17,5 @@ export declare class MetaDataUtil {
|
|
|
16
17
|
* Retrieve metadata record for a given sample/node name with fallback strategies.
|
|
17
18
|
*/
|
|
18
19
|
static readonly getRecordForSample: (metadata: MetadataParseResult | null | undefined, sampleId: string, matchKeyField?: null | string) => MetadataRecord | undefined;
|
|
19
|
-
static parseTsv
|
|
20
|
+
static parseTsv(content: string): MetadataParseResult;
|
|
20
21
|
}
|
|
@@ -42,6 +42,9 @@ var e = class e {
|
|
|
42
42
|
matchCounts: i
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
+
static getGroupId(e) {
|
|
46
|
+
return e?.columns?.includes("groupId") ? Object.keys(e.recordsByColumn.groupId)?.[0] ?? "" : "";
|
|
47
|
+
}
|
|
45
48
|
static getHighlightedIds(e, t, n, r = null) {
|
|
46
49
|
let i = e.trim().toLowerCase();
|
|
47
50
|
if (!i) return [];
|
|
@@ -81,7 +84,7 @@ var e = class e {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
|
-
static parseTsv
|
|
87
|
+
static parseTsv(e) {
|
|
85
88
|
if (!e?.trim()) return {
|
|
86
89
|
columns: [],
|
|
87
90
|
error: "Empty metadata content provided.",
|
|
@@ -125,7 +128,7 @@ var e = class e {
|
|
|
125
128
|
recordsByColumn: c,
|
|
126
129
|
recordsBySampleId: s
|
|
127
130
|
};
|
|
128
|
-
}
|
|
131
|
+
}
|
|
129
132
|
};
|
|
130
133
|
export { e as MetaDataUtil };
|
|
131
134
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["MetaDataUtil","matchCounts","columns","recordsByColumn","fastaIds","bestColumn","maxMatches","prioritizedKeys","col","count","currentPriority","newPriority","pKey","found","c","trimmed","query","matched","allAccessions","acc","metadataRecords","matchKeyField","rec","val","colDict","id","metadata","sampleId","\"displayName\"","\"accessionVersion\"","match","content","lines","headerLine","delimiter","records","recordsByAccession","recordsBySampleId","i","line","values","record","colIdx","accessionKey","sampleIdKey"],"sources":["../../../src/utils/MetaDataUtil/MetaDataUtil.ts"],"sourcesContent":["import type {\n MetadataParseResult,\n MetadataRecord,\n} from '../../models/metadata.models';\n\nexport class MetaDataUtil {\n /**\n * Compute the best matching metadata column for the given FASTA sequence IDs.\n */\n public static findBestMatchingColumn = (\n columns: string[],\n recordsByColumn: Record<string, Record<string, MetadataRecord>>,\n fastaIds: string[],\n ): { bestColumn: null | string; matchCounts: Record<string, number> } => {\n const matchCounts = MetaDataUtil.getMatchCounts(columns, recordsByColumn, fastaIds);\n let bestColumn: null | string = null;\n let maxMatches = -1;\n\n if (!columns || !recordsByColumn || !fastaIds || fastaIds.length === 0) {\n return { bestColumn: null, matchCounts: {} };\n }\n\n const prioritizedKeys = [\n 'displayname',\n 'accessionversion',\n 'accession',\n 'sampleid',\n 'sample_id',\n 'id',\n 'strain',\n 'isolate',\n 'submissionid',\n 'name',\n ];\n\n for (const col of columns) {\n const colDict = recordsByColumn[col];\n if (!colDict) {\n continue;\n }\n\n const count = matchCounts[col];\n\n if (count > maxMatches) {\n maxMatches = count;\n bestColumn = col;\n } else if (count === maxMatches && count > 0 && bestColumn) {\n // Tie-breaker: prioritize standard ID column names\n const currentPriority = prioritizedKeys.indexOf(bestColumn.toLowerCase());\n const newPriority = prioritizedKeys.indexOf(col.toLowerCase());\n if (newPriority !== -1 && (currentPriority === -1 || newPriority < currentPriority)) {\n bestColumn = col;\n }\n }\n }\n\n if (maxMatches <= 0) {\n // If no matches, default to highest priority known ID column if available\n for (const pKey of prioritizedKeys) {\n const found = columns.find((c) => c.toLowerCase() === pKey);\n if (found) {\n bestColumn = found;\n break;\n }\n }\n if (!bestColumn && columns.length > 0) {\n bestColumn = columns[0];\n }\n }\n\n return { bestColumn, matchCounts };\n };\n\n public static getHighlightedIds(query: string, allAccessions: string[], metadataRecords: MetadataRecord[], matchKeyField: null | string = null): string[] {\n const trimmed = query.trim().toLowerCase();\n if (!trimmed) {\n return [];\n }\n\n const matched = new Set<string>();\n\n // Check accession names / fasta IDs\n allAccessions.forEach((acc) => {\n if (acc.toLowerCase().includes(trimmed)) {\n matched.add(acc);\n }\n });\n\n // Check metadata records\n metadataRecords.forEach((rec) => {\n const acc =\n (matchKeyField && rec[matchKeyField]) ||\n rec.displayName ||\n rec.accessionVersion ||\n rec.accession ||\n rec.id ||\n rec.submissionId ||\n rec.SampleID;\n if (acc) {\n const matchesAnyField = Object.values(rec).some(\n (val) => typeof val === 'string' && val.toLowerCase().includes(trimmed),\n );\n if (matchesAnyField) {\n matched.add(acc);\n }\n }\n });\n\n return Array.from(matched);\n }\n /**\n * Count FASTA sequence ID matches for each metadata column.\n */\n public static getMatchCounts = (\n columns: string[],\n recordsByColumn: Record<string, Record<string, MetadataRecord>>,\n fastaIds: string[],\n ): Record<string, number> => {\n const matchCounts: Record<string, number> = {};\n\n if (!columns || !recordsByColumn || !fastaIds || fastaIds.length === 0) {\n return matchCounts;\n }\n\n for (const col of columns) {\n const colDict = recordsByColumn[col];\n if (!colDict) {\n continue;\n }\n\n let count = 0;\n for (const id of fastaIds) {\n if (colDict[id]) {\n count++;\n }\n }\n matchCounts[col] = count;\n }\n\n return matchCounts;\n };\n\n /**\n * Retrieve metadata record for a given sample/node name with fallback strategies.\n */\n public static readonly getRecordForSample = (\n metadata: MetadataParseResult | null | undefined,\n sampleId: string,\n matchKeyField?: null | string,\n ): MetadataRecord | undefined => {\n if (!metadata || !sampleId) {\n return undefined;\n }\n\n // 1. If matchKeyField is specified and exists, check it first\n if (matchKeyField && metadata.recordsByColumn?.[matchKeyField]) {\n const rec = metadata.recordsByColumn[matchKeyField][sampleId];\n if (rec) {\n return rec;\n }\n }\n\n // 2. Check displayName\n if (metadata.recordsByColumn?.['displayName']?.[sampleId]) {\n return metadata.recordsByColumn['displayName'][sampleId];\n }\n\n // 3. Check accessionVersion\n if (metadata.recordsByColumn?.['accessionVersion']?.[sampleId]) {\n return metadata.recordsByColumn['accessionVersion'][sampleId];\n }\n\n // 4. Check accession maps\n if (metadata.recordsByAccession[sampleId]) {\n return metadata.recordsByAccession[sampleId];\n }\n if (metadata.recordsBySampleId[sampleId]) {\n return metadata.recordsBySampleId[sampleId];\n }\n\n // 5. Broad search through all indexed columns\n if (metadata.recordsByColumn) {\n for (const col of Object.keys(metadata.recordsByColumn)) {\n const match = metadata.recordsByColumn[col][sampleId];\n if (match) {\n return match;\n }\n }\n }\n\n return undefined;\n };\n\n public static parseTsv = (content: string): MetadataParseResult => {\n if (!content?.trim()) {\n return {\n columns: [],\n error: 'Empty metadata content provided.',\n records: [],\n recordsByAccession: {},\n recordsByColumn: {},\n recordsBySampleId: {},\n };\n }\n\n const lines = content.trim().split('\\n');\n if (lines.length < 2) {\n return {\n columns: [],\n error: 'Metadata file must contain at least a header row and one data row.',\n records: [],\n recordsByAccession: {},\n recordsByColumn: {},\n recordsBySampleId: {},\n };\n }\n\n // Parse header row\n const headerLine = lines[0].trim();\n let delimiter = '\\t';\n if (!headerLine.includes('\\t') && headerLine.includes(',')) {\n delimiter = ',';\n }\n const columns = headerLine\n .split(delimiter)\n .map((col) => col.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((c) => c.length > 0);\n\n const records: MetadataRecord[] = [];\n const recordsByAccession: Record<string, MetadataRecord> = {};\n const recordsBySampleId: Record<string, MetadataRecord> = {};\n const recordsByColumn: Record<string, Record<string, MetadataRecord>> = {};\n\n columns.forEach((col) => {\n recordsByColumn[col] = {};\n });\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i].trim();\n if (!line) {\n continue;\n }\n\n const values = line.split(delimiter).map((val) => val.trim().replace(/^[\"']|[\"']$/g, ''));\n const record: MetadataRecord = {};\n\n columns.forEach((col, colIdx) => {\n const val = values[colIdx] ?? '';\n record[col] = val;\n if (val) {\n recordsByColumn[col][val] = record;\n }\n });\n\n records.push(record);\n\n // Index by potential ID keys\n const accessionKey =\n record.displayName ||\n record.accessionVersion ||\n record.accession ||\n record.id ||\n record.submissionId ||\n record.strain ||\n record.isolate ||\n record.name;\n\n if (accessionKey) {\n recordsByAccession[accessionKey] = record;\n }\n\n const sampleIdKey =\n record.SampleID ||\n record.sampleId ||\n record.sample_id ||\n record.specimenCollectorSampleId ||\n record.id;\n\n if (sampleIdKey) {\n recordsBySampleId[sampleIdKey] = record;\n }\n }\n\n return {\n columns,\n records,\n recordsByAccession,\n recordsByColumn,\n recordsBySampleId,\n };\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["MetaDataUtil","matchCounts","columns","recordsByColumn","fastaIds","bestColumn","maxMatches","prioritizedKeys","col","count","currentPriority","newPriority","pKey","found","c","metaData","trimmed","query","matched","allAccessions","acc","metadataRecords","matchKeyField","rec","val","colDict","id","metadata","sampleId","\"displayName\"","\"accessionVersion\"","match","content","lines","headerLine","delimiter","records","recordsByAccession","recordsBySampleId","i","line","values","record","colIdx","accessionKey","sampleIdKey"],"sources":["../../../src/utils/MetaDataUtil/MetaDataUtil.ts"],"sourcesContent":["import type {\n MetadataParseResult,\n MetadataRecord,\n} from '../../models/metadata.models';\n\nexport class MetaDataUtil {\n /**\n * Compute the best matching metadata column for the given FASTA sequence IDs.\n */\n public static findBestMatchingColumn = (\n columns: string[],\n recordsByColumn: Record<string, Record<string, MetadataRecord>>,\n fastaIds: string[],\n ): { bestColumn: null | string; matchCounts: Record<string, number> } => {\n const matchCounts = MetaDataUtil.getMatchCounts(columns, recordsByColumn, fastaIds);\n let bestColumn: null | string = null;\n let maxMatches = -1;\n\n if (!columns || !recordsByColumn || !fastaIds || fastaIds.length === 0) {\n return { bestColumn: null, matchCounts: {} };\n }\n\n const prioritizedKeys = [\n 'displayname',\n 'accessionversion',\n 'accession',\n 'sampleid',\n 'sample_id',\n 'id',\n 'strain',\n 'isolate',\n 'submissionid',\n 'name',\n ];\n\n for (const col of columns) {\n const colDict = recordsByColumn[col];\n if (!colDict) {\n continue;\n }\n\n const count = matchCounts[col];\n\n if (count > maxMatches) {\n maxMatches = count;\n bestColumn = col;\n } else if (count === maxMatches && count > 0 && bestColumn) {\n // Tie-breaker: prioritize standard ID column names\n const currentPriority = prioritizedKeys.indexOf(bestColumn.toLowerCase());\n const newPriority = prioritizedKeys.indexOf(col.toLowerCase());\n if (newPriority !== -1 && (currentPriority === -1 || newPriority < currentPriority)) {\n bestColumn = col;\n }\n }\n }\n\n if (maxMatches <= 0) {\n // If no matches, default to highest priority known ID column if available\n for (const pKey of prioritizedKeys) {\n const found = columns.find((c) => c.toLowerCase() === pKey);\n if (found) {\n bestColumn = found;\n break;\n }\n }\n if (!bestColumn && columns.length > 0) {\n bestColumn = columns[0];\n }\n }\n\n return { bestColumn, matchCounts };\n };\n\n public static getGroupId(metaData: MetadataParseResult): string {\n if (!metaData?.columns?.includes('groupId')) {\n return '';\n }\n\n return Object.keys(metaData.recordsByColumn.groupId)?.[0] ?? '';\n }\n\n public static getHighlightedIds(query: string, allAccessions: string[], metadataRecords: MetadataRecord[], matchKeyField: null | string = null): string[] {\n const trimmed = query.trim().toLowerCase();\n if (!trimmed) {\n return [];\n }\n\n const matched = new Set<string>();\n\n // Check accession names / fasta IDs\n allAccessions.forEach((acc) => {\n if (acc.toLowerCase().includes(trimmed)) {\n matched.add(acc);\n }\n });\n\n // Check metadata records\n metadataRecords.forEach((rec) => {\n const acc =\n (matchKeyField && rec[matchKeyField]) ||\n rec.displayName ||\n rec.accessionVersion ||\n rec.accession ||\n rec.id ||\n rec.submissionId ||\n rec.SampleID;\n if (acc) {\n const matchesAnyField = Object.values(rec).some(\n (val) => typeof val === 'string' && val.toLowerCase().includes(trimmed),\n );\n if (matchesAnyField) {\n matched.add(acc);\n }\n }\n });\n\n return Array.from(matched);\n }\n\n /**\n * Count FASTA sequence ID matches for each metadata column.\n */\n public static getMatchCounts = (\n columns: string[],\n recordsByColumn: Record<string, Record<string, MetadataRecord>>,\n fastaIds: string[],\n ): Record<string, number> => {\n const matchCounts: Record<string, number> = {};\n\n if (!columns || !recordsByColumn || !fastaIds || fastaIds.length === 0) {\n return matchCounts;\n }\n\n for (const col of columns) {\n const colDict = recordsByColumn[col];\n if (!colDict) {\n continue;\n }\n\n let count = 0;\n for (const id of fastaIds) {\n if (colDict[id]) {\n count++;\n }\n }\n matchCounts[col] = count;\n }\n\n return matchCounts;\n };\n\n /**\n * Retrieve metadata record for a given sample/node name with fallback strategies.\n */\n public static readonly getRecordForSample = (\n metadata: MetadataParseResult | null | undefined,\n sampleId: string,\n matchKeyField?: null | string,\n ): MetadataRecord | undefined => {\n if (!metadata || !sampleId) {\n return undefined;\n }\n\n // 1. If matchKeyField is specified and exists, check it first\n if (matchKeyField && metadata.recordsByColumn?.[matchKeyField]) {\n const rec = metadata.recordsByColumn[matchKeyField][sampleId];\n if (rec) {\n return rec;\n }\n }\n\n // 2. Check displayName\n if (metadata.recordsByColumn?.['displayName']?.[sampleId]) {\n return metadata.recordsByColumn['displayName'][sampleId];\n }\n\n // 3. Check accessionVersion\n if (metadata.recordsByColumn?.['accessionVersion']?.[sampleId]) {\n return metadata.recordsByColumn['accessionVersion'][sampleId];\n }\n\n // 4. Check accession maps\n if (metadata.recordsByAccession[sampleId]) {\n return metadata.recordsByAccession[sampleId];\n }\n if (metadata.recordsBySampleId[sampleId]) {\n return metadata.recordsBySampleId[sampleId];\n }\n\n // 5. Broad search through all indexed columns\n if (metadata.recordsByColumn) {\n for (const col of Object.keys(metadata.recordsByColumn)) {\n const match = metadata.recordsByColumn[col][sampleId];\n if (match) {\n return match;\n }\n }\n }\n\n return undefined;\n };\n\n public static parseTsv(content: string): MetadataParseResult {\n if (!content?.trim()) {\n return {\n columns: [],\n error: 'Empty metadata content provided.',\n records: [],\n recordsByAccession: {},\n recordsByColumn: {},\n recordsBySampleId: {},\n };\n }\n\n const lines = content.trim().split('\\n');\n if (lines.length < 2) {\n return {\n columns: [],\n error: 'Metadata file must contain at least a header row and one data row.',\n records: [],\n recordsByAccession: {},\n recordsByColumn: {},\n recordsBySampleId: {},\n };\n }\n\n // Parse header row\n const headerLine = lines[0].trim();\n let delimiter = '\\t';\n if (!headerLine.includes('\\t') && headerLine.includes(',')) {\n delimiter = ',';\n }\n const columns = headerLine\n .split(delimiter)\n .map((col) => col.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((c) => c.length > 0);\n\n const records: MetadataRecord[] = [];\n const recordsByAccession: Record<string, MetadataRecord> = {};\n const recordsBySampleId: Record<string, MetadataRecord> = {};\n const recordsByColumn: Record<string, Record<string, MetadataRecord>> = {};\n\n columns.forEach((col) => {\n recordsByColumn[col] = {};\n });\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i].trim();\n if (!line) {\n continue;\n }\n\n const values = line.split(delimiter).map((val) => val.trim().replace(/^[\"']|[\"']$/g, ''));\n const record: MetadataRecord = {};\n\n columns.forEach((col, colIdx) => {\n const val = values[colIdx] ?? '';\n record[col] = val;\n if (val) {\n recordsByColumn[col][val] = record;\n }\n });\n\n records.push(record);\n\n // Index by potential ID keys\n const accessionKey =\n record.displayName ||\n record.accessionVersion ||\n record.accession ||\n record.id ||\n record.submissionId ||\n record.strain ||\n record.isolate ||\n record.name;\n\n if (accessionKey) {\n recordsByAccession[accessionKey] = record;\n }\n\n const sampleIdKey =\n record.SampleID ||\n record.sampleId ||\n record.sample_id ||\n record.specimenCollectorSampleId ||\n record.id;\n\n if (sampleIdKey) {\n recordsBySampleId[sampleIdKey] = record;\n }\n }\n\n return {\n columns,\n records,\n recordsByAccession,\n recordsByColumn,\n recordsBySampleId,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { WindowService as t } from "@gen-epix/ui-core/classes/services/WindowSer
|
|
|
4
4
|
var n = class {
|
|
5
5
|
static clearSearchParams() {
|
|
6
6
|
let n = new URL(e.resolve(t).window.location.href);
|
|
7
|
-
n.search = "", e.resolve(t).window.history.replaceState(null, "", n.toString());
|
|
7
|
+
n.search = "", e.resolve(t).window.history.replaceState(null, "", n.toString()), e.resolve(t).window.dispatchEvent(new Event("loculus:urlparamschange"));
|
|
8
8
|
}
|
|
9
9
|
static fastaLabelFromUrl(e) {
|
|
10
10
|
try {
|
|
@@ -16,17 +16,16 @@ var n = class {
|
|
|
16
16
|
}
|
|
17
17
|
static getBaseURLWithPath() {
|
|
18
18
|
let n = new URL(e.resolve(t).window.location.href);
|
|
19
|
-
return `${n.protocol}//${n.host}/${n.pathname.split("/").filter(Boolean).shift()}`;
|
|
19
|
+
return n.pathname ? `${n.protocol}//${n.host}/${n.pathname.split("/").filter(Boolean).shift()}` : `${n.protocol}//${n.host}`;
|
|
20
20
|
}
|
|
21
|
-
static
|
|
21
|
+
static isValidResourceUrl(e) {
|
|
22
22
|
let t;
|
|
23
23
|
try {
|
|
24
24
|
t = new URL(e.trim());
|
|
25
25
|
} catch {
|
|
26
|
-
|
|
26
|
+
return !1;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
return t.href;
|
|
28
|
+
return t.protocol === "http:" || t.protocol === "https:";
|
|
30
29
|
}
|
|
31
30
|
};
|
|
32
31
|
export { n as UrlUtil };
|