@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.
Files changed (74) hide show
  1. package/esm/ConfigurationEditorWidget/components/AddNewField.d.ts +6 -0
  2. package/esm/ConfigurationEditorWidget/components/AddNewField.js +29 -0
  3. package/esm/ConfigurationEditorWidget/components/BooleanEditor.d.ts +3 -2
  4. package/esm/ConfigurationEditorWidget/components/BooleanEditor.js +6 -4
  5. package/esm/ConfigurationEditorWidget/components/CallbackEditor.d.ts +4 -2
  6. package/esm/ConfigurationEditorWidget/components/CallbackEditor.js +24 -42
  7. package/esm/ConfigurationEditorWidget/components/ColorEditor.d.ts +5 -12
  8. package/esm/ConfigurationEditorWidget/components/ColorEditor.js +47 -22
  9. package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.d.ts +2 -3
  10. package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.js +168 -29
  11. package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.d.ts +1 -1
  12. package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.js +10 -2
  13. package/esm/ConfigurationEditorWidget/components/EntryListEditor.d.ts +17 -0
  14. package/esm/ConfigurationEditorWidget/components/EntryListEditor.js +35 -0
  15. package/esm/ConfigurationEditorWidget/components/HeadingComponent.d.ts +1 -1
  16. package/esm/ConfigurationEditorWidget/components/IntegerEditor.d.ts +10 -0
  17. package/esm/ConfigurationEditorWidget/components/IntegerEditor.js +9 -0
  18. package/esm/ConfigurationEditorWidget/components/JsonEditor.d.ts +2 -1
  19. package/esm/ConfigurationEditorWidget/components/JsonEditor.js +24 -41
  20. package/esm/ConfigurationEditorWidget/components/MapEntryCard.d.ts +8 -0
  21. package/esm/ConfigurationEditorWidget/components/MapEntryCard.js +23 -0
  22. package/esm/ConfigurationEditorWidget/components/MapSlotEditor.d.ts +8 -0
  23. package/esm/ConfigurationEditorWidget/components/MapSlotEditor.js +18 -0
  24. package/esm/ConfigurationEditorWidget/components/NumberEditor.d.ts +5 -4
  25. package/esm/ConfigurationEditorWidget/components/NumberEditor.js +15 -15
  26. package/esm/ConfigurationEditorWidget/components/NumberMapEditor.d.ts +4 -4
  27. package/esm/ConfigurationEditorWidget/components/NumberMapEditor.js +7 -31
  28. package/esm/ConfigurationEditorWidget/components/SlotEditor.d.ts +4 -5
  29. package/esm/ConfigurationEditorWidget/components/SlotEditor.js +88 -49
  30. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.d.ts +16 -10
  31. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.js +22 -25
  32. package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.d.ts +3 -6
  33. package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.js +14 -39
  34. package/esm/ConfigurationEditorWidget/components/StringMapEditor.d.ts +10 -0
  35. package/esm/ConfigurationEditorWidget/components/StringMapEditor.js +13 -0
  36. package/esm/ConfigurationEditorWidget/components/TypeSelector.d.ts +2 -2
  37. package/esm/ConfigurationEditorWidget/components/TypeSelector.js +2 -2
  38. package/esm/ConfigurationEditorWidget/components/singularSlotName.d.ts +13 -0
  39. package/esm/ConfigurationEditorWidget/components/singularSlotName.js +23 -0
  40. package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.d.ts +2 -2
  41. package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.js +6 -1
  42. package/esm/ConfigurationEditorWidget/index.js +4 -4
  43. package/esm/ConfigurationEditorWidget/model.d.ts +8 -1
  44. package/esm/ConfigurationEditorWidget/model.js +40 -1
  45. package/esm/FromConfigAdapter/FromConfigAdapter.d.ts +6 -1
  46. package/esm/FromConfigAdapter/FromConfigAdapter.js +31 -1
  47. package/esm/FromConfigAdapter/configSchema.d.ts +32 -4
  48. package/esm/FromConfigAdapter/configSchema.js +34 -2
  49. package/esm/FromConfigAdapter/index.js +2 -2
  50. package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.d.ts +8 -0
  51. package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.js +10 -25
  52. package/esm/FromConfigRegionsAdapter/configSchema.d.ts +38 -4
  53. package/esm/FromConfigRegionsAdapter/configSchema.js +39 -2
  54. package/esm/FromConfigRegionsAdapter/index.js +2 -2
  55. package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.d.ts +15 -2
  56. package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.js +32 -40
  57. package/esm/FromConfigSequenceAdapter/configSchema.d.ts +37 -4
  58. package/esm/FromConfigSequenceAdapter/configSchema.js +38 -2
  59. package/esm/FromConfigSequenceAdapter/index.js +2 -2
  60. package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.d.ts +5 -4
  61. package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.js +28 -24
  62. package/esm/NcbiSequenceReportAliasAdapter/configSchema.d.ts +69 -10
  63. package/esm/NcbiSequenceReportAliasAdapter/configSchema.js +82 -18
  64. package/esm/NcbiSequenceReportAliasAdapter/index.js +3 -2
  65. package/esm/RefNameAliasAdapter/RefNameAliasAdapter.d.ts +4 -3
  66. package/esm/RefNameAliasAdapter/RefNameAliasAdapter.js +22 -28
  67. package/esm/RefNameAliasAdapter/configSchema.d.ts +70 -12
  68. package/esm/RefNameAliasAdapter/configSchema.js +89 -18
  69. package/esm/RefNameAliasAdapter/index.js +3 -2
  70. package/esm/aliasUtils.d.ts +4 -0
  71. package/esm/aliasUtils.js +19 -0
  72. package/esm/index.d.ts +2 -2
  73. package/esm/index.js +7 -7
  74. package/package.json +11 -15
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ export default function AddNewField({ onAdd, testid, startAdornment, }: {
3
+ onAdd: (value: string) => void;
4
+ testid?: string;
5
+ startAdornment?: ReactNode;
6
+ }): import("react").JSX.Element;
@@ -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/jsx-runtime").JSX.Element;
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 { Checkbox, FormControl, FormControlLabel, FormHelperText, } from '@mui/material';
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(FormControlLabel, { label: slot.name, control: _jsx(Checkbox, { checked: slot.value, onChange: evt => {
6
- slot.set(evt.target.checked);
7
- } }) }), _jsx(FormHelperText, { children: slot.description })] }));
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/jsx-runtime").JSX.Element;
11
+ }) => import("react").JSX.Element;
10
12
  export default CallbackEditor;
@@ -1,68 +1,50 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useTransition } from 'react';
3
- import { stringToJexlExpression } from '@jbrowse/core/util/jexlStrings';
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, TextField, Tooltip } from '@mui/material';
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.startsWith('jexl:') ? code : `jexl:${code}`;
32
- if (jexlCode === 'jexl:') {
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({ e });
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 [error, setCodeError] = useState();
37
+ const [codeError, setCodeError] = useState();
48
38
  const [, startTransition] = useTransition();
49
- return (_jsxs(_Fragment, { children: [error ? _jsx("p", { className: classes.error, children: `${error}` }) : null, _jsxs("div", { className: classes.callbackContainer, children: [_jsx(TextField, { multiline: true, className: classes.callbackEditor, value: code.startsWith('jexl:') ? code.split('jexl:')[1] : code, onChange: event => {
50
- const value = event.target.value;
51
- setCode(value);
52
- startTransition(() => {
53
- validateAndSetCode(value, slot, setCodeError);
54
- });
55
- }, style: { background: error ? '#fdd' : undefined }, slotProps: {
56
- input: {
57
- classes: {
58
- input: classes.textAreaFont,
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
- export declare const ColorSlot: (props: {
2
- value: string;
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/jsx-runtime").JSX.Element;
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 ColorPicker from '@jbrowse/core/ui/ColorPicker';
4
- import { TextField } from '@mui/material';
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
- export const ColorSlot = (props) => {
7
- const { value, label = '', TextFieldProps = {}, onChange } = props;
8
- const [displayed, setDisplayed] = useState(false);
9
- return (_jsxs("div", { style: { display: 'flex' }, children: [_jsx(TextField, { value: value, label: label, onClick: () => {
10
- setDisplayed(!displayed);
11
- }, onChange: event => {
12
- onChange(event.target.value);
13
- }, ...TextFieldProps }), _jsx("div", { style: { marginTop: 10 }, children: _jsx(Suspense, { fallback: null, children: _jsx(ColorPicker, { color: value, onChange: event => {
14
- onChange(event);
15
- } }) }) })] }));
16
- };
17
- const ColorEditor = observer(function ColorEditor(props) {
18
- const { slot } = props;
19
- return (_jsx(ColorSlot, { label: slot.name, value: slot.value, onChange: color => {
20
- slot.set(color);
21
- }, TextFieldProps: {
22
- helperText: slot.description,
23
- fullWidth: true,
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
- session?: AbstractSessionModel;
8
- }) => import("react/jsx-runtime").JSX.Element;
7
+ }) => import("react").JSX.Element;
9
8
  export default ConfigurationEditor;
@@ -1,18 +1,22 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { getTypeNamesFromExplicitlyTypedUnion, isConfigurationSchemaType, isConfigurationSlotType, readConfObject, } from '@jbrowse/core/configuration';
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 { Accordion, AccordionDetails, AccordionSummary, FormGroup, Typography, } from '@mui/material';
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 { singular } from 'pluralize';
10
- import SlotEditor from "./SlotEditor.js";
11
- import TypeSelector from "./TypeSelector.js";
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
- if (slot.length) {
33
- return slot.map((subslot, slotIndex) => {
34
- const key = subslot.type
35
- ? `${singular(slotName)} ${subslot.type}`
36
- : `${singular(slotName)} ${slotIndex + 1}`;
37
- return _jsx(Member, { ...props, slot: subslot, slotName: key }, key);
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, { defaultExpanded: true, className: classes.accordion, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMoreIcon, { className: classes.icon }), children: _jsx(Typography, { children: [...path, slotName].join('➔') }) }), _jsxs(AccordionDetails, { className: classes.expansionPanelDetails, children: [typeNameChoices.length ? (_jsx(TypeSelector, { typeNameChoices: typeNameChoices, slotName: slotName, slot: slot, onChange: evt => {
42
- if (evt.target.value !== slot.type) {
43
- schema.setSubschema(slotName, {
44
- type: evt.target.value,
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(FormGroup, { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName] }) })] })] }));
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 (isConfigurationSlotType(slotSchema)) {
50
- return _jsx(SlotEditor, { slot: slot, slotSchema: slotSchema }, slotName);
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
- const Schema = observer(function Schema({ schema, path = [], }) {
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
- return (_jsx(_Fragment, { children: Object.entries(properties).map(([slotName, slotSchema]) => (_jsx(Member, { slotName: slotName, slotSchema: slotSchema, path: path, schema: schema }, slotName))) }));
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, { className: classes.icon }), children: _jsx(Typography, { children: _jsx(SanitizedHTML, { html: name ?? 'Configuration' }) }) }), _jsx(AccordionDetails, { className: classes.expansionPanelDetails, "data-testid": "configEditor", children: _jsx(Schema, { schema: target }) })] }, key));
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,4 +1,4 @@
1
1
  import type { TextFieldProps } from '@mui/material';
2
2
  export default function ConfigurationTextField(props: {
3
3
  helperText?: string;
4
- } & TextFieldProps): import("react/jsx-runtime").JSX.Element;
4
+ } & TextFieldProps): import("react").JSX.Element;
@@ -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, { ...props, helperText: _jsx(SanitizedHTML, { html: helperText || '' }), fullWidth: true, slotProps: {
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 };
@@ -4,5 +4,5 @@ declare const HeadingComponent: ({ model, }: {
4
4
  type: string;
5
5
  };
6
6
  };
7
- }) => string | import("react/jsx-runtime").JSX.Element;
7
+ }) => string | import("react").JSX.Element;
8
8
  export default HeadingComponent;