@jbrowse/plugin-config 4.3.0 → 5.0.0-beta.10
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/esm/ConfigurationEditorWidget/components/AddNewField.d.ts +6 -0
- package/esm/ConfigurationEditorWidget/components/AddNewField.js +29 -0
- package/esm/ConfigurationEditorWidget/components/BooleanEditor.d.ts +3 -2
- package/esm/ConfigurationEditorWidget/components/BooleanEditor.js +6 -4
- package/esm/ConfigurationEditorWidget/components/CallbackEditor.d.ts +4 -2
- package/esm/ConfigurationEditorWidget/components/CallbackEditor.js +24 -42
- package/esm/ConfigurationEditorWidget/components/ColorEditor.d.ts +5 -12
- package/esm/ConfigurationEditorWidget/components/ColorEditor.js +47 -22
- package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.d.ts +2 -3
- package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.js +168 -29
- package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.d.ts +1 -1
- package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.js +10 -2
- package/esm/ConfigurationEditorWidget/components/EntryListEditor.d.ts +17 -0
- package/esm/ConfigurationEditorWidget/components/EntryListEditor.js +35 -0
- package/esm/ConfigurationEditorWidget/components/HeadingComponent.d.ts +1 -1
- package/esm/ConfigurationEditorWidget/components/IntegerEditor.d.ts +10 -0
- package/esm/ConfigurationEditorWidget/components/IntegerEditor.js +9 -0
- package/esm/ConfigurationEditorWidget/components/JsonEditor.d.ts +2 -1
- package/esm/ConfigurationEditorWidget/components/JsonEditor.js +24 -41
- package/esm/ConfigurationEditorWidget/components/MapEntryCard.d.ts +8 -0
- package/esm/ConfigurationEditorWidget/components/MapEntryCard.js +23 -0
- package/esm/ConfigurationEditorWidget/components/MapSlotEditor.d.ts +8 -0
- package/esm/ConfigurationEditorWidget/components/MapSlotEditor.js +18 -0
- package/esm/ConfigurationEditorWidget/components/NumberEditor.d.ts +5 -4
- package/esm/ConfigurationEditorWidget/components/NumberEditor.js +15 -15
- package/esm/ConfigurationEditorWidget/components/NumberMapEditor.d.ts +4 -4
- package/esm/ConfigurationEditorWidget/components/NumberMapEditor.js +7 -31
- package/esm/ConfigurationEditorWidget/components/SlotEditor.d.ts +4 -5
- package/esm/ConfigurationEditorWidget/components/SlotEditor.js +88 -49
- package/esm/ConfigurationEditorWidget/components/StringArrayEditor.d.ts +16 -10
- package/esm/ConfigurationEditorWidget/components/StringArrayEditor.js +22 -25
- package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.d.ts +3 -6
- package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.js +14 -39
- package/esm/ConfigurationEditorWidget/components/StringMapEditor.d.ts +10 -0
- package/esm/ConfigurationEditorWidget/components/StringMapEditor.js +13 -0
- package/esm/ConfigurationEditorWidget/components/TypeSelector.d.ts +2 -2
- package/esm/ConfigurationEditorWidget/components/TypeSelector.js +2 -2
- package/esm/ConfigurationEditorWidget/components/singularSlotName.d.ts +13 -0
- package/esm/ConfigurationEditorWidget/components/singularSlotName.js +23 -0
- package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.d.ts +2 -2
- package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.js +6 -1
- package/esm/ConfigurationEditorWidget/index.js +4 -4
- package/esm/ConfigurationEditorWidget/model.d.ts +8 -1
- package/esm/ConfigurationEditorWidget/model.js +40 -1
- package/esm/FromConfigAdapter/FromConfigAdapter.d.ts +6 -1
- package/esm/FromConfigAdapter/FromConfigAdapter.js +31 -1
- package/esm/FromConfigAdapter/configSchema.d.ts +32 -4
- package/esm/FromConfigAdapter/configSchema.js +34 -2
- package/esm/FromConfigAdapter/index.js +2 -2
- package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.d.ts +8 -0
- package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.js +10 -25
- package/esm/FromConfigRegionsAdapter/configSchema.d.ts +38 -4
- package/esm/FromConfigRegionsAdapter/configSchema.js +39 -2
- package/esm/FromConfigRegionsAdapter/index.js +2 -2
- package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.d.ts +15 -2
- package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.js +32 -40
- package/esm/FromConfigSequenceAdapter/configSchema.d.ts +37 -4
- package/esm/FromConfigSequenceAdapter/configSchema.js +38 -2
- package/esm/FromConfigSequenceAdapter/index.js +2 -2
- package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.d.ts +5 -4
- package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.js +28 -24
- package/esm/NcbiSequenceReportAliasAdapter/configSchema.d.ts +69 -10
- package/esm/NcbiSequenceReportAliasAdapter/configSchema.js +82 -18
- package/esm/NcbiSequenceReportAliasAdapter/index.js +3 -2
- package/esm/RefNameAliasAdapter/RefNameAliasAdapter.d.ts +4 -3
- package/esm/RefNameAliasAdapter/RefNameAliasAdapter.js +22 -28
- package/esm/RefNameAliasAdapter/configSchema.d.ts +70 -12
- package/esm/RefNameAliasAdapter/configSchema.js +89 -18
- package/esm/RefNameAliasAdapter/index.js +3 -2
- package/esm/aliasUtils.d.ts +4 -0
- package/esm/aliasUtils.js +19 -0
- package/esm/index.d.ts +2 -2
- package/esm/index.js +7 -7
- package/package.json +11 -15
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
4
|
+
import { IconButton, InputAdornment, TextField } from '@mui/material';
|
|
5
|
+
// shared "add new" entry field used by the string-array and map slot editors:
|
|
6
|
+
// a text box that commits its contents via onAdd and clears itself
|
|
7
|
+
export default function AddNewField({ onAdd, testid, startAdornment, }) {
|
|
8
|
+
const [value, setValue] = useState('');
|
|
9
|
+
function commit() {
|
|
10
|
+
if (value !== '') {
|
|
11
|
+
onAdd(value);
|
|
12
|
+
setValue('');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return (_jsx(TextField, { fullWidth: true, value: value, placeholder: "add new", onChange: event => {
|
|
16
|
+
setValue(event.target.value);
|
|
17
|
+
}, onKeyDown: event => {
|
|
18
|
+
if (event.key === 'Enter') {
|
|
19
|
+
commit();
|
|
20
|
+
}
|
|
21
|
+
}, slotProps: {
|
|
22
|
+
input: {
|
|
23
|
+
startAdornment,
|
|
24
|
+
endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { "data-testid": testid, disabled: value === '', onClick: () => {
|
|
25
|
+
commit();
|
|
26
|
+
}, children: _jsx(AddIcon, {}) }) })),
|
|
27
|
+
},
|
|
28
|
+
} }));
|
|
29
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
/** #slotEditor checkbox */
|
|
1
2
|
declare const BooleanEditor: ({ slot, }: {
|
|
2
3
|
slot: {
|
|
3
4
|
name: string;
|
|
4
|
-
value: boolean;
|
|
5
|
+
value: boolean | undefined;
|
|
5
6
|
set: (arg: boolean) => void;
|
|
6
7
|
description: string;
|
|
7
8
|
};
|
|
8
|
-
}) => import("react
|
|
9
|
+
}) => import("react").JSX.Element;
|
|
9
10
|
export default BooleanEditor;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { LabeledCheckbox } from '@jbrowse/core/ui';
|
|
3
|
+
import { FormControl, FormHelperText } from '@mui/material';
|
|
3
4
|
import { observer } from 'mobx-react';
|
|
5
|
+
/** #slotEditor checkbox */
|
|
4
6
|
const BooleanEditor = observer(function BooleanEditor({ slot, }) {
|
|
5
|
-
return (_jsxs(FormControl, { children: [_jsx(
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
return (_jsxs(FormControl, { children: [_jsx(LabeledCheckbox, { label: slot.name, checked: !!slot.value, onChange: val => {
|
|
8
|
+
slot.set(val);
|
|
9
|
+
} }), _jsx(FormHelperText, { children: slot.description })] }));
|
|
8
10
|
});
|
|
9
11
|
export default BooleanEditor;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type PluginManager from '@jbrowse/core/PluginManager';
|
|
1
2
|
declare const CallbackEditor: ({ slot, }: {
|
|
2
3
|
slot: {
|
|
3
4
|
set: (arg: string) => void;
|
|
4
5
|
description: string;
|
|
5
6
|
name: string;
|
|
6
7
|
value: string;
|
|
7
|
-
contextVariable: string;
|
|
8
|
+
contextVariable: string[];
|
|
9
|
+
pluginManager: PluginManager;
|
|
8
10
|
};
|
|
9
|
-
}) => import("react
|
|
11
|
+
}) => import("react").JSX.Element;
|
|
10
12
|
export default CallbackEditor;
|
|
@@ -1,68 +1,50 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useTransition } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { MonospaceTextField } from '@jbrowse/core/ui';
|
|
4
|
+
import { ensureJexlPrefix, stringToJexlExpression, } from '@jbrowse/core/util/jexlStrings';
|
|
4
5
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import { getEnv } from '@jbrowse/mobx-state-tree';
|
|
6
6
|
import HelpIcon from '@mui/icons-material/Help';
|
|
7
|
-
import { IconButton,
|
|
7
|
+
import { IconButton, Tooltip } from '@mui/material';
|
|
8
8
|
import { observer } from 'mobx-react';
|
|
9
|
-
const fontFamily = 'Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace';
|
|
10
9
|
const useStyles = makeStyles()(theme => ({
|
|
11
10
|
callbackEditor: {
|
|
12
11
|
marginTop: '16px',
|
|
13
12
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
14
13
|
width: '100%',
|
|
15
|
-
fontFamily,
|
|
16
|
-
},
|
|
17
|
-
textAreaFont: {
|
|
18
|
-
fontFamily,
|
|
19
|
-
},
|
|
20
|
-
callbackContainer: {
|
|
21
|
-
width: '100%',
|
|
22
|
-
overflowX: 'auto',
|
|
23
|
-
},
|
|
24
|
-
error: {
|
|
25
|
-
color: 'red',
|
|
26
|
-
fontSize: '0.8em',
|
|
27
14
|
},
|
|
28
15
|
}));
|
|
29
16
|
function validateAndSetCode(code, slot, setCodeError) {
|
|
17
|
+
// empty buffer is "in progress", not invalid — don't commit and don't warn
|
|
18
|
+
const trimmed = code.trim();
|
|
19
|
+
if (trimmed === '' || trimmed === 'jexl:') {
|
|
20
|
+
setCodeError(undefined);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
30
23
|
try {
|
|
31
|
-
const jexlCode = code
|
|
32
|
-
|
|
33
|
-
throw new Error('Empty jexl expression is not valid');
|
|
34
|
-
}
|
|
35
|
-
stringToJexlExpression(jexlCode, getEnv(slot).pluginManager?.jexl);
|
|
24
|
+
const jexlCode = ensureJexlPrefix(code);
|
|
25
|
+
stringToJexlExpression(jexlCode, slot.pluginManager.jexl);
|
|
36
26
|
slot.set(jexlCode);
|
|
37
27
|
setCodeError(undefined);
|
|
38
28
|
}
|
|
39
29
|
catch (e) {
|
|
40
|
-
console.error(
|
|
30
|
+
console.error(e);
|
|
41
31
|
setCodeError(e);
|
|
42
32
|
}
|
|
43
33
|
}
|
|
44
34
|
const CallbackEditor = observer(function CallbackEditor({ slot, }) {
|
|
45
35
|
const { classes } = useStyles();
|
|
46
36
|
const [code, setCode] = useState(slot.value);
|
|
47
|
-
const [
|
|
37
|
+
const [codeError, setCodeError] = useState();
|
|
48
38
|
const [, startTransition] = useTransition();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
} }), _jsx("p", { children: slot.description }), _jsx(Tooltip, { title: _jsxs("div", { children: ["Callbacks are written in Jexl format. Click to learn more.", _jsx("br", {}), " Names of available context items: ", slot.contextVariable] }), arrow: true, children: _jsx(IconButton, { color: "primary", onClick: () => {
|
|
62
|
-
const newWindow = window.open('https://github.com/TomFrost/Jexl', '_blank', 'noopener,noreferrer');
|
|
63
|
-
if (newWindow) {
|
|
64
|
-
newWindow.opener = null;
|
|
65
|
-
}
|
|
66
|
-
}, children: _jsx(HelpIcon, {}) }) })] })] }));
|
|
39
|
+
// if default value is a callback, will have to remove jexl:
|
|
40
|
+
// do this last
|
|
41
|
+
return (_jsxs(MonospaceTextField, { className: classes.callbackEditor, value: code, error: codeError, onChange: value => {
|
|
42
|
+
setCode(value);
|
|
43
|
+
startTransition(() => {
|
|
44
|
+
validateAndSetCode(value, slot, setCodeError);
|
|
45
|
+
});
|
|
46
|
+
}, children: [_jsx("p", { children: slot.description }), _jsx(Tooltip, { title: _jsxs("div", { children: ["Callbacks are written in Jexl format. Click to learn more.", _jsx("br", {}), " Names of available context items:", ' ', slot.contextVariable.join(', ')] }), arrow: true, children: _jsx(IconButton, { color: "primary", onClick: () => {
|
|
47
|
+
window.open('https://jbrowse.org/jb2/docs/config_guides/jexl/', '_blank', 'noopener,noreferrer');
|
|
48
|
+
}, children: _jsx(HelpIcon, {}) }) })] }));
|
|
67
49
|
});
|
|
68
50
|
export default CallbackEditor;
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
label?: string;
|
|
4
|
-
TextFieldProps?: {
|
|
5
|
-
helperText: string;
|
|
6
|
-
fullWidth: boolean;
|
|
7
|
-
};
|
|
8
|
-
onChange: (arg: string) => void;
|
|
9
|
-
}) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
declare const ColorEditor: (props: {
|
|
1
|
+
/** #slotEditor text field beside a swatch that opens a color picker */
|
|
2
|
+
declare const ColorEditor: ({ slot, }: {
|
|
11
3
|
slot: {
|
|
12
4
|
name: string;
|
|
13
|
-
value: string;
|
|
5
|
+
value: string | undefined;
|
|
14
6
|
description: string;
|
|
7
|
+
contextVariable?: string[];
|
|
15
8
|
set: (arg: string) => void;
|
|
16
9
|
};
|
|
17
|
-
}) => import("react
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
18
11
|
export default ColorEditor;
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense, useState } from 'react';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { Suspense, lazy, useState } from 'react';
|
|
3
|
+
import { isCssColor } from '@jbrowse/core/util/colorBits';
|
|
4
|
+
import { isJexl } from '@jbrowse/core/util/jexlStrings';
|
|
5
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
6
|
import { observer } from 'mobx-react';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
import ConfigurationTextField from './ConfigurationTextField.js';
|
|
8
|
+
const PopoverPicker = lazy(() => import('@jbrowse/core/ui/PopoverPicker'));
|
|
9
|
+
const useStyles = makeStyles()({
|
|
10
|
+
root: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
gap: 4,
|
|
14
|
+
},
|
|
15
|
+
field: {
|
|
16
|
+
flex: 1,
|
|
17
|
+
minWidth: 0,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
// Also drives `maybeColor`, whose value is `undefined` while unset. An empty
|
|
21
|
+
// string keeps the field controlled and the picker fed, the same way
|
|
22
|
+
// NumberEditor renders an unset `maybeNumber` as an empty field. The slot
|
|
23
|
+
// refuses what is not a color, so text that does not parse yet stays a draft
|
|
24
|
+
// in the field and reaches the slot once it does; leaving the field drops an
|
|
25
|
+
// unparsed draft. "Unset" is reachable through the slot's reset button, which
|
|
26
|
+
// is where every other slot type puts it. Only a slot declaring a
|
|
27
|
+
// `contextVariable` takes a `jexl:` callback, which an entry of a colour list
|
|
28
|
+
// never does.
|
|
29
|
+
/** #slotEditor text field beside a swatch that opens a color picker */
|
|
30
|
+
const ColorEditor = observer(function ColorEditor({ slot, }) {
|
|
31
|
+
const admitsJexl = !!slot.contextVariable?.length;
|
|
32
|
+
const { classes } = useStyles();
|
|
33
|
+
const [draft, setDraft] = useState();
|
|
34
|
+
const value = slot.value ?? '';
|
|
35
|
+
return (_jsxs("div", { className: classes.root, children: [_jsx(ConfigurationTextField, { value: draft ?? value, label: slot.name, error: draft !== undefined, helperText: draft === undefined ? slot.description : `"${draft}" is not a color`, className: classes.field, onChange: event => {
|
|
36
|
+
const text = event.target.value;
|
|
37
|
+
if (isCssColor(text) || (admitsJexl && isJexl(text))) {
|
|
38
|
+
slot.set(text);
|
|
39
|
+
setDraft(undefined);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
setDraft(text);
|
|
43
|
+
}
|
|
44
|
+
}, onBlur: () => {
|
|
45
|
+
setDraft(undefined);
|
|
46
|
+
} }), _jsx(Suspense, { fallback: null, children: _jsx(PopoverPicker, { color: value, onChange: color => {
|
|
47
|
+
slot.set(color);
|
|
48
|
+
setDraft(undefined);
|
|
49
|
+
} }) })] }));
|
|
25
50
|
});
|
|
26
51
|
export default ColorEditor;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
|
|
2
|
-
import type { AbstractSessionModel } from '@jbrowse/core/util';
|
|
3
2
|
declare const ConfigurationEditor: ({ model, }: {
|
|
4
3
|
model: {
|
|
5
4
|
target?: AnyConfigurationModel;
|
|
5
|
+
expandedDisplayId?: string;
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
}) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
}) => import("react").JSX.Element;
|
|
9
8
|
export default ConfigurationEditor;
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { arraySlotUnion, getSlotDefinition, getTypeNamesFromExplicitlyTypedUnion, isConfigurationModel, isConfigurationSchemaType, isConfigurationSlot, makeSlotFacade, readConfObject, setConf, } from '@jbrowse/core/configuration';
|
|
3
4
|
import SanitizedHTML from '@jbrowse/core/ui/SanitizedHTML';
|
|
4
5
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import { getMembers } from '@jbrowse/mobx-state-tree';
|
|
6
|
+
import { getMembers, getSnapshot } from '@jbrowse/mobx-state-tree';
|
|
7
|
+
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
8
|
+
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
9
|
+
import ClearIcon from '@mui/icons-material/Clear';
|
|
10
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
11
|
+
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
|
6
12
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
7
|
-
import
|
|
13
|
+
import SearchIcon from '@mui/icons-material/Search';
|
|
14
|
+
import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, InputAdornment, MenuItem, TextField, Typography, } from '@mui/material';
|
|
8
15
|
import { observer } from 'mobx-react';
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
16
|
+
import SlotEditor from './SlotEditor.js';
|
|
17
|
+
import TypeSelector from './TypeSelector.js';
|
|
18
|
+
import { singularSlotName } from './singularSlotName.js';
|
|
12
19
|
const useStyles = makeStyles()(theme => ({
|
|
13
|
-
icon: {
|
|
14
|
-
color: theme.palette.tertiary.contrastText || '#fff',
|
|
15
|
-
},
|
|
16
20
|
expansionPanelDetails: {
|
|
17
21
|
display: 'block',
|
|
18
22
|
padding: theme.spacing(1),
|
|
@@ -24,47 +28,182 @@ const useStyles = makeStyles()(theme => ({
|
|
|
24
28
|
width: '100%',
|
|
25
29
|
overflowX: 'auto',
|
|
26
30
|
},
|
|
31
|
+
filter: {
|
|
32
|
+
margin: theme.spacing(1),
|
|
33
|
+
},
|
|
34
|
+
noResults: {
|
|
35
|
+
margin: theme.spacing(2),
|
|
36
|
+
fontStyle: 'italic',
|
|
37
|
+
},
|
|
38
|
+
advancedToggle: {
|
|
39
|
+
margin: theme.spacing(1, 0),
|
|
40
|
+
textTransform: 'none',
|
|
41
|
+
},
|
|
42
|
+
entryControls: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
justifyContent: 'flex-end',
|
|
45
|
+
},
|
|
27
46
|
}));
|
|
47
|
+
// The entries of an array of sub-schemas as snapshots, for a whole-list write.
|
|
48
|
+
function listSnapshot(schema, listName) {
|
|
49
|
+
return [...getSnapshot(schema[listName])];
|
|
50
|
+
}
|
|
51
|
+
// matches a slot or sub-schema against a lowercased filter string, recursing
|
|
52
|
+
// into sub-schemas so a nested slot name keeps its ancestors visible
|
|
53
|
+
function memberMatches(schema, slotName, slotSchema, query) {
|
|
54
|
+
if (isConfigurationSlot(schema, slotName)) {
|
|
55
|
+
const { description } = getSlotDefinition(schema, slotName);
|
|
56
|
+
return (slotName.toLowerCase().includes(query) ||
|
|
57
|
+
!!description?.toLowerCase().includes(query));
|
|
58
|
+
}
|
|
59
|
+
else if (isConfigurationSchemaType(slotSchema)) {
|
|
60
|
+
const slot = schema[slotName];
|
|
61
|
+
return (slotName.toLowerCase().includes(query) ||
|
|
62
|
+
(Array.isArray(slot)
|
|
63
|
+
? slot.some(subslot => schemaMatches(subslot, query))
|
|
64
|
+
: slot !== undefined && schemaMatches(slot, query)));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function schemaMatches(schema, query) {
|
|
71
|
+
const { properties } = getMembers(schema);
|
|
72
|
+
return Object.entries(properties).some(([slotName, slotSchema]) => memberMatches(schema, slotName, slotSchema, query));
|
|
73
|
+
}
|
|
74
|
+
// a slot flagged `advanced` in its schema definition, hidden behind a toggle
|
|
75
|
+
function isAdvancedSlot(schema, slotName) {
|
|
76
|
+
return (isConfigurationSlot(schema, slotName) &&
|
|
77
|
+
!!getSlotDefinition(schema, slotName).advanced);
|
|
78
|
+
}
|
|
79
|
+
// a display config is the only sub-schema carrying a displayId; when the editor
|
|
80
|
+
// was opened from a view, only the active display expands by default so the
|
|
81
|
+
// track's other (incompatible/inactive) displays don't crowd the panel
|
|
82
|
+
function displayDefaultExpanded(slot, expandedDisplayId) {
|
|
83
|
+
const displayId = isConfigurationModel(slot)
|
|
84
|
+
? readConfObject(slot, 'displayId')
|
|
85
|
+
: undefined;
|
|
86
|
+
return !displayId || !expandedDisplayId || displayId === expandedDisplayId;
|
|
87
|
+
}
|
|
88
|
+
// A union list entry's own controls: its type, and its place in the list. Each
|
|
89
|
+
// is one whole-list write, so every state it leaves loads again.
|
|
90
|
+
const EntryControls = observer(function EntryControls({ schema, title, slot, entry: { listName, choices, index, count }, }) {
|
|
91
|
+
const { classes } = useStyles();
|
|
92
|
+
const write = (edit) => {
|
|
93
|
+
setConf(schema, listName, edit(listSnapshot(schema, listName)));
|
|
94
|
+
};
|
|
95
|
+
const swap = (at) => {
|
|
96
|
+
write(list => list.toSpliced(at, 2, list[at + 1], list[at]));
|
|
97
|
+
};
|
|
98
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: classes.entryControls, children: [_jsx(IconButton, { "aria-label": `move ${title} up`, disabled: index === 0, onClick: () => {
|
|
99
|
+
swap(index - 1);
|
|
100
|
+
}, children: _jsx(ArrowUpwardIcon, { fontSize: "small" }) }), _jsx(IconButton, { "aria-label": `move ${title} down`, disabled: index === count - 1, onClick: () => {
|
|
101
|
+
swap(index);
|
|
102
|
+
}, children: _jsx(ArrowDownwardIcon, { fontSize: "small" }) }), _jsx(IconButton, { "aria-label": `remove ${title}`, onClick: () => {
|
|
103
|
+
write(list => list.toSpliced(index, 1));
|
|
104
|
+
}, children: _jsx(DeleteIcon, { fontSize: "small" }) })] }), _jsx(TypeSelector, { typeNameChoices: choices, slotName: title, slot: slot, onChange: evt => {
|
|
105
|
+
const type = evt.target.value;
|
|
106
|
+
if (type !== slot.type) {
|
|
107
|
+
write(list => list.with(index, { type }));
|
|
108
|
+
}
|
|
109
|
+
} })] }));
|
|
110
|
+
});
|
|
28
111
|
const Member = observer(function Member(props) {
|
|
29
112
|
const { classes } = useStyles();
|
|
30
|
-
const { slotName, slotSchema, schema, slot = schema[slotName], path = [], } = props;
|
|
113
|
+
const { slotName, slotSchema, schema, slot = schema[slotName], path = [], filter = '', expandedDisplayId, entry, } = props;
|
|
114
|
+
// when the sub-schema's own name matches, drop the filter for its children so
|
|
115
|
+
// the whole group stays visible; otherwise keep filtering descendants
|
|
116
|
+
const childFilter = slotName.toLowerCase().includes(filter.toLowerCase())
|
|
117
|
+
? ''
|
|
118
|
+
: filter;
|
|
31
119
|
if (isConfigurationSchemaType(slotSchema)) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
120
|
+
// an array-typed sub-schema (e.g. `displays`) renders one Member per entry;
|
|
121
|
+
// a single sub-schema (not an array) falls through to the accordion below.
|
|
122
|
+
// keyed on Array.isArray, not `.length` truthiness, so an empty array maps
|
|
123
|
+
// to nothing instead of being mis-handled as a single schema
|
|
124
|
+
if (Array.isArray(slot)) {
|
|
125
|
+
return _jsx(EntryList, { ...props, entries: slot });
|
|
39
126
|
}
|
|
127
|
+
// if this is an explicitly typed schema, make a type-selecting dropdown
|
|
128
|
+
// that can be used to change its type
|
|
40
129
|
const typeNameChoices = getTypeNamesFromExplicitlyTypedUnion(slotSchema);
|
|
41
|
-
return (_jsxs(Accordion
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
130
|
+
return (_jsxs(Accordion
|
|
131
|
+
// an active filter force-expands the group so a match inside an
|
|
132
|
+
// otherwise-collapsed (inactive display) sub-schema is actually
|
|
133
|
+
// visible, not just present in the collapsed DOM. The key flips the
|
|
134
|
+
// accordion between filtered/unfiltered mounts so the new
|
|
135
|
+
// defaultExpanded takes effect (Accordion reads it only on mount).
|
|
136
|
+
, { defaultExpanded: filter ? true : displayDefaultExpanded(slot, expandedDisplayId), className: classes.accordion, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMoreIcon, {}), children: _jsx(Typography, { children: [...path, slotName].join('➔') }) }), _jsxs(AccordionDetails, { className: classes.expansionPanelDetails, children: [entry && slot ? (_jsx(EntryControls, { schema: schema, title: slotName, slot: slot, entry: entry })) : null, typeNameChoices.length ? (_jsx(TypeSelector, { typeNameChoices: typeNameChoices, slotName: slotName, slot: slot, onChange: evt => {
|
|
137
|
+
if (evt.target.value !== slot?.type) {
|
|
138
|
+
setConf(schema, slotName, { type: evt.target.value });
|
|
46
139
|
}
|
|
47
|
-
} })) : null, _jsx(
|
|
140
|
+
} })) : null, slot ? (_jsx("div", { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName], filter: childFilter, expandedDisplayId: expandedDisplayId }) })) : null] })] }, filter ? 'filtered' : 'unfiltered'));
|
|
48
141
|
}
|
|
49
|
-
else if (
|
|
50
|
-
return _jsx(SlotEditor, { slot:
|
|
142
|
+
else if (isConfigurationSlot(schema, slotName)) {
|
|
143
|
+
return _jsx(SlotEditor, { slot: makeSlotFacade(schema, slotName) }, slotName);
|
|
51
144
|
}
|
|
52
145
|
else {
|
|
53
146
|
return null;
|
|
54
147
|
}
|
|
55
148
|
});
|
|
56
|
-
|
|
149
|
+
// An array of sub-schemas, one accordion per entry. A list whose entries are a
|
|
150
|
+
// `ConfigurationSchemaUnion` also offers each entry's type and place, and a new
|
|
151
|
+
// entry of any member type; a pluggable union such as a track's `displays` is
|
|
152
|
+
// not one, since a display is added through the track.
|
|
153
|
+
const EntryList = observer(function EntryList(props) {
|
|
154
|
+
const { classes } = useStyles();
|
|
155
|
+
const { slotName, slotSchema, schema, entries } = props;
|
|
156
|
+
const singular = singularSlotName(slotName);
|
|
157
|
+
const union = arraySlotUnion(slotSchema);
|
|
158
|
+
const choices = union ? Object.keys(union.members) : [];
|
|
159
|
+
return (_jsxs(_Fragment, { children: [entries.map((subslot, index) => {
|
|
160
|
+
const title = `${singular} ${index + 1}`;
|
|
161
|
+
return (_jsx(Member
|
|
162
|
+
// eslint-disable-next-line @eslint-react/no-array-index-key -- a config entry has no identity but its place
|
|
163
|
+
, { ...props, slot: subslot, slotName: union || !subslot.type ? title : `${title} ${subslot.type}`, entry: union
|
|
164
|
+
? { listName: slotName, choices, index, count: entries.length }
|
|
165
|
+
: undefined }, index));
|
|
166
|
+
}), union ? (_jsx(TextField, { className: classes.filter, select: true, size: "small", label: `Add ${singular}`, value: "", onChange: evt => {
|
|
167
|
+
setConf(schema, slotName, [
|
|
168
|
+
...listSnapshot(schema, slotName),
|
|
169
|
+
{ type: evt.target.value },
|
|
170
|
+
]);
|
|
171
|
+
}, fullWidth: true, children: choices.map(type => (_jsx(MenuItem, { value: type, children: type }, type))) })) : null] }));
|
|
172
|
+
});
|
|
173
|
+
const Schema = observer(function Schema({ schema, path = [], filter = '', expandedDisplayId, }) {
|
|
174
|
+
const { classes } = useStyles();
|
|
175
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
176
|
+
const query = filter.toLowerCase();
|
|
57
177
|
const properties = getMembers(schema).properties;
|
|
58
|
-
|
|
178
|
+
const visible = Object.entries(properties).filter(([slotName, slotSchema]) => !query || memberMatches(schema, slotName, slotSchema, query));
|
|
179
|
+
const normal = visible.filter(([slotName]) => !isAdvancedSlot(schema, slotName));
|
|
180
|
+
const advanced = visible.filter(([slotName]) => isAdvancedSlot(schema, slotName));
|
|
181
|
+
const renderMember = ([slotName, slotSchema]) => (_jsx(Member, { slotName: slotName, slotSchema: slotSchema, path: path, schema: schema, filter: filter, expandedDisplayId: expandedDisplayId }, slotName));
|
|
182
|
+
return (_jsxs(_Fragment, { children: [normal.map(renderMember), advanced.length && !query ? (_jsxs(Button, { className: classes.advancedToggle, size: "small", startIcon: showAdvanced ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: () => {
|
|
183
|
+
setShowAdvanced(!showAdvanced);
|
|
184
|
+
}, children: [showAdvanced ? 'Hide' : 'Show', " advanced settings (", advanced.length, ")"] })) : null, showAdvanced || query ? advanced.map(renderMember) : null] }));
|
|
59
185
|
});
|
|
60
186
|
const ConfigurationEditor = observer(function ConfigurationEditor({ model, }) {
|
|
61
187
|
const { classes } = useStyles();
|
|
188
|
+
const [filter, setFilter] = useState('');
|
|
189
|
+
// key forces a re-render, otherwise the same field can end up being used for
|
|
190
|
+
// different tracks since only the backing model changes for example see pr
|
|
191
|
+
// #804
|
|
62
192
|
const target = model.target;
|
|
63
193
|
if (!target) {
|
|
64
194
|
return _jsx(Typography, { children: "No configuration target" });
|
|
65
195
|
}
|
|
66
196
|
const key = readConfObject(target, 'trackId');
|
|
67
197
|
const name = readConfObject(target, 'name');
|
|
68
|
-
return (_jsxs(Accordion, { defaultExpanded: true, className: classes.accordion, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMoreIcon, {
|
|
198
|
+
return (_jsxs(Accordion, { defaultExpanded: true, className: classes.accordion, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMoreIcon, {}), children: _jsx(Typography, { children: _jsx(SanitizedHTML, { html: name ?? 'Configuration' }) }) }), _jsxs(AccordionDetails, { className: classes.expansionPanelDetails, "data-testid": "configEditor", children: [_jsx(TextField, { className: classes.filter, label: "Filter options", value: filter, onChange: evt => {
|
|
199
|
+
setFilter(evt.target.value);
|
|
200
|
+
}, size: "small", fullWidth: true, slotProps: {
|
|
201
|
+
input: {
|
|
202
|
+
startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small" }) })),
|
|
203
|
+
endAdornment: filter ? (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { size: "small", "aria-label": "clear filter", onClick: () => {
|
|
204
|
+
setFilter('');
|
|
205
|
+
}, children: _jsx(ClearIcon, { fontSize: "small" }) }) })) : null,
|
|
206
|
+
},
|
|
207
|
+
} }), filter && !schemaMatches(target, filter.toLowerCase()) ? (_jsxs(Typography, { className: classes.noResults, children: ["No options match \u201C", filter, "\u201D"] })) : (_jsx(Schema, { schema: target, filter: filter, expandedDisplayId: model.expandedDisplayId }))] })] }, key));
|
|
69
208
|
});
|
|
70
209
|
export default ConfigurationEditor;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { SanitizedHTML } from '@jbrowse/core/ui';
|
|
3
3
|
import { TextField } from '@mui/material';
|
|
4
|
+
// adds ability to have html in helperText. note that FormHelperTextProps is
|
|
5
|
+
// div because the default is p which does not like div children
|
|
4
6
|
export default function ConfigurationTextField(props) {
|
|
5
|
-
const { helperText } = props;
|
|
6
|
-
return (_jsx(TextField, { ...
|
|
7
|
+
const { helperText, slotProps, ...rest } = props;
|
|
8
|
+
return (_jsx(TextField, { ...rest, helperText: _jsx(SanitizedHTML, { html: helperText || '' }), fullWidth: true,
|
|
9
|
+
// merged, not replaced: this component owns `formHelperText` (the div
|
|
10
|
+
// above needs it) while a caller owns the rest, and setting the whole bag
|
|
11
|
+
// here dropped a caller's `inputLabel` silently — the shrink that lets a
|
|
12
|
+
// labelled field draw its placeholder
|
|
13
|
+
slotProps: {
|
|
14
|
+
...slotProps,
|
|
7
15
|
formHelperText: {
|
|
8
16
|
component: 'div',
|
|
9
17
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface ListSlot {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string[];
|
|
4
|
+
description: string;
|
|
5
|
+
set: (arg: string[]) => void;
|
|
6
|
+
}
|
|
7
|
+
/** #slotEditor a text field and color picker per entry, with add and delete */
|
|
8
|
+
declare const ColorArrayEditor: ({ slot, }: {
|
|
9
|
+
slot: ListSlot;
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
11
|
+
/** #slotEditor a dropdown of the `model`'s members per entry, with add and delete */
|
|
12
|
+
declare const StringEnumArrayEditor: ({ slot, }: {
|
|
13
|
+
slot: ListSlot & {
|
|
14
|
+
choices?: string[];
|
|
15
|
+
};
|
|
16
|
+
}) => import("react").JSX.Element;
|
|
17
|
+
export { ColorArrayEditor, StringEnumArrayEditor };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
3
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
4
|
+
import { FormHelperText, IconButton, InputLabel, List, ListItem, MenuItem, } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import ColorEditor from './ColorEditor.js';
|
|
7
|
+
import ConfigurationTextField from './ConfigurationTextField.js';
|
|
8
|
+
// A list slot whose entries each take one control: the list is written whole
|
|
9
|
+
// on every change, and "add" appends an entry the slot already admits, so no
|
|
10
|
+
// write can hold an entry the slot refuses.
|
|
11
|
+
const EntryList = observer(function EntryList({ slot, entry, added, }) {
|
|
12
|
+
const value = [...slot.value];
|
|
13
|
+
const addition = added(value);
|
|
14
|
+
return (_jsxs(_Fragment, { children: [slot.name ? _jsx(InputLabel, { children: slot.name }) : null, _jsxs(List, { disablePadding: true, children: [value.map((v, idx) => (_jsxs(ListItem, { disableGutters: true, children: [entry(v, next => {
|
|
15
|
+
slot.set(value.map((old, i) => (i === idx ? next : old)));
|
|
16
|
+
}), _jsx(IconButton, { "aria-label": "delete entry", onClick: () => {
|
|
17
|
+
slot.set(value.filter((_, i) => i !== idx));
|
|
18
|
+
}, children: _jsx(DeleteIcon, {}) })] }, idx))), _jsx(ListItem, { disableGutters: true, children: _jsx(IconButton, { "data-testid": `entryAdd-${slot.name}`, "aria-label": "add entry", disabled: addition === undefined, onClick: () => {
|
|
19
|
+
if (addition !== undefined) {
|
|
20
|
+
slot.set([...value, addition]);
|
|
21
|
+
}
|
|
22
|
+
}, children: _jsx(AddIcon, {}) }) })] }), _jsx(FormHelperText, { children: slot.description })] }));
|
|
23
|
+
});
|
|
24
|
+
/** #slotEditor a text field and color picker per entry, with add and delete */
|
|
25
|
+
const ColorArrayEditor = observer(function ColorArrayEditor({ slot, }) {
|
|
26
|
+
return (_jsx(EntryList, { slot: slot, added: value => value.at(-1) ?? 'black', entry: (color, set) => (_jsx(ColorEditor, { slot: { name: '', description: '', value: color, set } })) }));
|
|
27
|
+
});
|
|
28
|
+
/** #slotEditor a dropdown of the `model`'s members per entry, with add and delete */
|
|
29
|
+
const StringEnumArrayEditor = observer(function StringEnumArrayEditor({ slot, }) {
|
|
30
|
+
const choices = slot.choices ?? [];
|
|
31
|
+
return (_jsx(EntryList, { slot: slot, added: () => choices[0], entry: (member, set) => (_jsx(ConfigurationTextField, { select: true, value: member, onChange: evt => {
|
|
32
|
+
set(evt.target.value);
|
|
33
|
+
}, children: choices.map(choice => (_jsx(MenuItem, { value: choice, children: choice }, choice))) })) }));
|
|
34
|
+
});
|
|
35
|
+
export { ColorArrayEditor, StringEnumArrayEditor };
|