@jbrowse/plugin-config 4.2.1 → 5.0.0-beta.1

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 (72) hide show
  1. package/esm/ConfigurationEditorWidget/components/AddNewField.d.ts +4 -0
  2. package/esm/ConfigurationEditorWidget/components/AddNewField.js +28 -0
  3. package/esm/ConfigurationEditorWidget/components/BooleanEditor.d.ts +4 -2
  4. package/esm/ConfigurationEditorWidget/components/BooleanEditor.js +19 -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 +3 -11
  8. package/esm/ConfigurationEditorWidget/components/ColorEditor.js +28 -20
  9. package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.d.ts +2 -3
  10. package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.js +108 -17
  11. package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.d.ts +1 -1
  12. package/esm/ConfigurationEditorWidget/components/ConfigurationTextField.js +10 -2
  13. package/esm/ConfigurationEditorWidget/components/HeadingComponent.d.ts +1 -1
  14. package/esm/ConfigurationEditorWidget/components/IntegerEditor.d.ts +10 -0
  15. package/esm/ConfigurationEditorWidget/components/IntegerEditor.js +9 -0
  16. package/esm/ConfigurationEditorWidget/components/JsonEditor.d.ts +3 -1
  17. package/esm/ConfigurationEditorWidget/components/JsonEditor.js +27 -40
  18. package/esm/ConfigurationEditorWidget/components/MapEntryCard.d.ts +8 -0
  19. package/esm/ConfigurationEditorWidget/components/MapEntryCard.js +23 -0
  20. package/esm/ConfigurationEditorWidget/components/MapSlotEditor.d.ts +8 -0
  21. package/esm/ConfigurationEditorWidget/components/MapSlotEditor.js +18 -0
  22. package/esm/ConfigurationEditorWidget/components/NumberEditor.d.ts +6 -4
  23. package/esm/ConfigurationEditorWidget/components/NumberEditor.js +24 -15
  24. package/esm/ConfigurationEditorWidget/components/NumberMapEditor.d.ts +4 -4
  25. package/esm/ConfigurationEditorWidget/components/NumberMapEditor.js +7 -31
  26. package/esm/ConfigurationEditorWidget/components/SlotEditor.d.ts +4 -5
  27. package/esm/ConfigurationEditorWidget/components/SlotEditor.js +83 -48
  28. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.d.ts +3 -4
  29. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.js +11 -24
  30. package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.d.ts +3 -6
  31. package/esm/ConfigurationEditorWidget/components/StringArrayMapEditor.js +14 -39
  32. package/esm/ConfigurationEditorWidget/components/TypeSelector.d.ts +1 -1
  33. package/esm/ConfigurationEditorWidget/components/TypeSelector.js +2 -2
  34. package/esm/ConfigurationEditorWidget/components/singularSlotName.d.ts +13 -0
  35. package/esm/ConfigurationEditorWidget/components/singularSlotName.js +23 -0
  36. package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.d.ts +2 -2
  37. package/esm/ConfigurationEditorWidget/components/useSlotEditorStyles.js +6 -1
  38. package/esm/ConfigurationEditorWidget/index.js +3 -3
  39. package/esm/ConfigurationEditorWidget/model.d.ts +8 -1
  40. package/esm/ConfigurationEditorWidget/model.js +40 -1
  41. package/esm/FromConfigAdapter/FromConfigAdapter.d.ts +6 -1
  42. package/esm/FromConfigAdapter/FromConfigAdapter.js +31 -1
  43. package/esm/FromConfigAdapter/configSchema.d.ts +36 -3
  44. package/esm/FromConfigAdapter/configSchema.js +34 -2
  45. package/esm/FromConfigAdapter/index.js +2 -2
  46. package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.d.ts +8 -0
  47. package/esm/FromConfigRegionsAdapter/FromConfigRegionsAdapter.js +10 -25
  48. package/esm/FromConfigRegionsAdapter/configSchema.d.ts +42 -3
  49. package/esm/FromConfigRegionsAdapter/configSchema.js +39 -2
  50. package/esm/FromConfigRegionsAdapter/index.js +2 -2
  51. package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.d.ts +15 -2
  52. package/esm/FromConfigSequenceAdapter/FromConfigSequenceAdapter.js +32 -40
  53. package/esm/FromConfigSequenceAdapter/configSchema.d.ts +41 -3
  54. package/esm/FromConfigSequenceAdapter/configSchema.js +38 -2
  55. package/esm/FromConfigSequenceAdapter/index.js +2 -2
  56. package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.d.ts +5 -4
  57. package/esm/NcbiSequenceReportAliasAdapter/NcbiSequenceReportAliasAdapter.js +28 -24
  58. package/esm/NcbiSequenceReportAliasAdapter/configSchema.d.ts +77 -9
  59. package/esm/NcbiSequenceReportAliasAdapter/configSchema.js +79 -12
  60. package/esm/NcbiSequenceReportAliasAdapter/index.js +3 -2
  61. package/esm/RefNameAliasAdapter/RefNameAliasAdapter.d.ts +4 -3
  62. package/esm/RefNameAliasAdapter/RefNameAliasAdapter.js +22 -28
  63. package/esm/RefNameAliasAdapter/configSchema.d.ts +87 -12
  64. package/esm/RefNameAliasAdapter/configSchema.js +86 -12
  65. package/esm/RefNameAliasAdapter/index.js +3 -2
  66. package/esm/aliasUtils.d.ts +4 -0
  67. package/esm/aliasUtils.js +26 -0
  68. package/esm/index.d.ts +2 -2
  69. package/esm/index.js +7 -7
  70. package/esm/normalizeUriSnapshot.d.ts +1 -0
  71. package/esm/normalizeUriSnapshot.js +13 -0
  72. package/package.json +11 -15
@@ -0,0 +1,4 @@
1
+ export default function AddNewField({ onAdd, testid, }: {
2
+ onAdd: (value: string) => void;
3
+ testid?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
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, }) {
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
+ endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { "data-testid": testid, disabled: value === '', onClick: () => {
24
+ commit();
25
+ }, children: _jsx(AddIcon, {}) }) })),
26
+ },
27
+ } }));
28
+ }
@@ -1,9 +1,11 @@
1
+ /** #slotEditor checkbox */
1
2
  declare const BooleanEditor: ({ slot, }: {
2
3
  slot: {
3
4
  name: string;
4
- value: boolean;
5
+ value: boolean | undefined;
6
+ promotedBase?: unknown;
5
7
  set: (arg: boolean) => void;
6
8
  description: string;
7
9
  };
8
- }) => import("react/jsx-runtime").JSX.Element;
10
+ }) => import("react").JSX.Element;
9
11
  export default BooleanEditor;
@@ -1,9 +1,24 @@
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,
8
+ // An unset promotable slot renders its `promotedBase`, not `false`.
9
+ // Coercing every unset slot to unchecked mis-stated the three whose
10
+ // base is `true` (displayDirectionalChevrons, showSashimiArcs,
11
+ // readConnectionsDown): the box read unchecked on a track that was
12
+ // drawing chevrons, and ticking it wrote the value the track already
13
+ // had — no visible change, but the slot silently left the cascade and
14
+ // stopped following later session-wide defaults. The reset button
15
+ // (SlotEditor's `modified`) is what still separates inherited from
16
+ // customized; this only fixes which state is *shown*. The session tier
17
+ // is out of reach here, so a value promoted this session isn't
18
+ // reflected — see agent-docs/ideas/promotable-slot-ui.md, which is
19
+ // where that raw-vs-resolved gap is worked out.
20
+ checked: slot.value ?? !!slot.promotedBase, onChange: val => {
21
+ slot.set(val);
22
+ } }), _jsx(FormHelperText, { children: slot.description })] }));
8
23
  });
9
24
  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,10 @@
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;
1
+ /** #slotEditor text field beside a swatch that opens a color picker */
10
2
  declare const ColorEditor: (props: {
11
3
  slot: {
12
4
  name: string;
13
- value: string;
5
+ value: string | undefined;
14
6
  description: string;
15
7
  set: (arg: string) => void;
16
8
  };
17
- }) => import("react/jsx-runtime").JSX.Element;
9
+ }) => import("react").JSX.Element;
18
10
  export default ColorEditor;
@@ -1,26 +1,34 @@
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 } from 'react';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
4
  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
- };
5
+ import ConfigurationTextField from './ConfigurationTextField.js';
6
+ const PopoverPicker = lazy(() => import('@jbrowse/core/ui/PopoverPicker'));
7
+ const useStyles = makeStyles()({
8
+ root: {
9
+ display: 'flex',
10
+ alignItems: 'center',
11
+ gap: 4,
12
+ },
13
+ field: {
14
+ flex: 1,
15
+ minWidth: 0,
16
+ },
17
+ });
18
+ // Also drives `maybeColor`, whose value is `undefined` while unset. An empty
19
+ // string keeps the field controlled and the picker fed, the same way
20
+ // NumberEditor renders an unset `maybeNumber` as an empty field. Clearing the
21
+ // field writes `''` rather than restoring `undefined` — "unset" is reachable
22
+ // through the slot's reset button, which is where every other slot type puts it.
23
+ /** #slotEditor text field beside a swatch that opens a color picker */
17
24
  const ColorEditor = observer(function ColorEditor(props) {
18
25
  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
- } }));
26
+ const { classes } = useStyles();
27
+ const value = slot.value ?? '';
28
+ return (_jsxs("div", { className: classes.root, children: [_jsx(ConfigurationTextField, { value: value, label: slot.name, helperText: slot.description, className: classes.field, onChange: event => {
29
+ slot.set(event.target.value);
30
+ } }), _jsx(Suspense, { fallback: null, children: _jsx(PopoverPicker, { color: value, onChange: color => {
31
+ slot.set(color);
32
+ } }) })] }));
25
33
  });
26
34
  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,14 +1,18 @@
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 { getSlotDefinition, getTypeNamesFromExplicitlyTypedUnion, isConfigurationModel, isConfigurationSchemaType, isConfigurationSlot, makeSlotFacade, readConfObject, } from '@jbrowse/core/configuration';
3
4
  import SanitizedHTML from '@jbrowse/core/ui/SanitizedHTML';
4
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
6
  import { getMembers } from '@jbrowse/mobx-state-tree';
7
+ import ClearIcon from '@mui/icons-material/Clear';
8
+ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
6
9
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
7
- import { Accordion, AccordionDetails, AccordionSummary, FormGroup, Typography, } from '@mui/material';
10
+ import SearchIcon from '@mui/icons-material/Search';
11
+ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, InputAdornment, TextField, Typography, } from '@mui/material';
8
12
  import { observer } from 'mobx-react';
9
- import { singular } from 'pluralize';
10
- import SlotEditor from "./SlotEditor.js";
11
- import TypeSelector from "./TypeSelector.js";
13
+ import SlotEditor from './SlotEditor.js';
14
+ import TypeSelector from './TypeSelector.js';
15
+ import { singularSlotName } from './singularSlotName.js';
12
16
  const useStyles = makeStyles()(theme => ({
13
17
  icon: {
14
18
  color: theme.palette.tertiary.contrastText || '#fff',
@@ -24,47 +28,134 @@ 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
+ },
27
42
  }));
43
+ // matches a slot or sub-schema against a lowercased filter string, recursing
44
+ // into sub-schemas so a nested slot name keeps its ancestors visible
45
+ function memberMatches(schema, slotName, slotSchema, query) {
46
+ if (isConfigurationSlot(schema, slotName)) {
47
+ const { description } = getSlotDefinition(schema, slotName);
48
+ return (slotName.toLowerCase().includes(query) ||
49
+ !!description?.toLowerCase().includes(query));
50
+ }
51
+ else if (isConfigurationSchemaType(slotSchema)) {
52
+ const slot = schema[slotName];
53
+ return (slotName.toLowerCase().includes(query) ||
54
+ (Array.isArray(slot)
55
+ ? slot.some(subslot => schemaMatches(subslot, query))
56
+ : schemaMatches(slot, query)));
57
+ }
58
+ else {
59
+ return false;
60
+ }
61
+ }
62
+ function schemaMatches(schema, query) {
63
+ const { properties } = getMembers(schema);
64
+ return Object.entries(properties).some(([slotName, slotSchema]) => memberMatches(schema, slotName, slotSchema, query));
65
+ }
66
+ // a slot flagged `advanced` in its schema definition, hidden behind a toggle
67
+ function isAdvancedSlot(schema, slotName) {
68
+ return (isConfigurationSlot(schema, slotName) &&
69
+ !!getSlotDefinition(schema, slotName).advanced);
70
+ }
71
+ // a display config is the only sub-schema carrying a displayId; when the editor
72
+ // was opened from a view, only the active display expands by default so the
73
+ // track's other (incompatible/inactive) displays don't crowd the panel
74
+ function displayDefaultExpanded(slot, expandedDisplayId) {
75
+ const displayId = isConfigurationModel(slot)
76
+ ? readConfObject(slot, 'displayId')
77
+ : undefined;
78
+ return !displayId || !expandedDisplayId || displayId === expandedDisplayId;
79
+ }
28
80
  const Member = observer(function Member(props) {
29
81
  const { classes } = useStyles();
30
- const { slotName, slotSchema, schema, slot = schema[slotName], path = [], } = props;
82
+ const { slotName, slotSchema, schema, slot = schema[slotName], path = [], filter = '', expandedDisplayId, } = props;
83
+ // when the sub-schema's own name matches, drop the filter for its children so
84
+ // the whole group stays visible; otherwise keep filtering descendants
85
+ const childFilter = slotName.toLowerCase().includes(filter.toLowerCase())
86
+ ? ''
87
+ : filter;
31
88
  if (isConfigurationSchemaType(slotSchema)) {
32
- if (slot.length) {
89
+ // an array-typed sub-schema (e.g. `displays`) renders one Member per entry;
90
+ // a single sub-schema (not an array) falls through to the accordion below.
91
+ // keyed on Array.isArray, not `.length` truthiness, so an empty array maps
92
+ // to nothing instead of being mis-handled as a single schema
93
+ if (Array.isArray(slot)) {
33
94
  return slot.map((subslot, slotIndex) => {
34
95
  const key = subslot.type
35
- ? `${singular(slotName)} ${subslot.type}`
36
- : `${singular(slotName)} ${slotIndex + 1}`;
96
+ ? `${singularSlotName(slotName)} ${subslot.type}`
97
+ : `${singularSlotName(slotName)} ${slotIndex + 1}`;
37
98
  return _jsx(Member, { ...props, slot: subslot, slotName: key }, key);
38
99
  });
39
100
  }
101
+ // if this is an explicitly typed schema, make a type-selecting dropdown
102
+ // that can be used to change its type
40
103
  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 => {
104
+ return (_jsxs(Accordion
105
+ // an active filter force-expands the group so a match inside an
106
+ // otherwise-collapsed (inactive display) sub-schema is actually
107
+ // visible, not just present in the collapsed DOM. The key flips the
108
+ // accordion between filtered/unfiltered mounts so the new
109
+ // defaultExpanded takes effect (Accordion reads it only on mount).
110
+ , { defaultExpanded: filter ? true : displayDefaultExpanded(slot, expandedDisplayId), 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
111
  if (evt.target.value !== slot.type) {
43
112
  schema.setSubschema(slotName, {
44
113
  type: evt.target.value,
45
114
  });
46
115
  }
47
- } })) : null, _jsx(FormGroup, { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName] }) })] })] }));
116
+ } })) : null, _jsx("div", { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName], filter: childFilter, expandedDisplayId: expandedDisplayId }) })] })] }, filter ? 'filtered' : 'unfiltered'));
48
117
  }
49
- else if (isConfigurationSlotType(slotSchema)) {
50
- return _jsx(SlotEditor, { slot: slot, slotSchema: slotSchema }, slotName);
118
+ else if (isConfigurationSlot(schema, slotName)) {
119
+ return _jsx(SlotEditor, { slot: makeSlotFacade(schema, slotName) }, slotName);
51
120
  }
52
121
  else {
53
122
  return null;
54
123
  }
55
124
  });
56
- const Schema = observer(function Schema({ schema, path = [], }) {
125
+ const Schema = observer(function Schema({ schema, path = [], filter = '', expandedDisplayId, }) {
126
+ const { classes } = useStyles();
127
+ const [showAdvanced, setShowAdvanced] = useState(false);
128
+ const query = filter.toLowerCase();
57
129
  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))) }));
130
+ const visible = Object.entries(properties).filter(([slotName, slotSchema]) => !query || memberMatches(schema, slotName, slotSchema, query));
131
+ const normal = visible.filter(([slotName]) => !isAdvancedSlot(schema, slotName));
132
+ const advanced = visible.filter(([slotName]) => isAdvancedSlot(schema, slotName));
133
+ const renderMember = ([slotName, slotSchema]) => (_jsx(Member, { slotName: slotName, slotSchema: slotSchema, path: path, schema: schema, filter: filter, expandedDisplayId: expandedDisplayId }, slotName));
134
+ return (_jsxs(_Fragment, { children: [normal.map(renderMember), advanced.length && !query ? (_jsxs(Button, { className: classes.advancedToggle, size: "small", startIcon: showAdvanced ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: () => {
135
+ setShowAdvanced(!showAdvanced);
136
+ }, children: [showAdvanced ? 'Hide' : 'Show', " advanced settings (", advanced.length, ")"] })) : null, showAdvanced || query ? advanced.map(renderMember) : null] }));
59
137
  });
60
138
  const ConfigurationEditor = observer(function ConfigurationEditor({ model, }) {
61
139
  const { classes } = useStyles();
140
+ const [filter, setFilter] = useState('');
141
+ // key forces a re-render, otherwise the same field can end up being used for
142
+ // different tracks since only the backing model changes for example see pr
143
+ // #804
62
144
  const target = model.target;
63
145
  if (!target) {
64
146
  return _jsx(Typography, { children: "No configuration target" });
65
147
  }
66
148
  const key = readConfObject(target, 'trackId');
67
149
  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));
150
+ 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' }) }) }), _jsxs(AccordionDetails, { className: classes.expansionPanelDetails, "data-testid": "configEditor", children: [_jsx(TextField, { className: classes.filter, label: "Filter options", value: filter, onChange: evt => {
151
+ setFilter(evt.target.value);
152
+ }, size: "small", fullWidth: true, slotProps: {
153
+ input: {
154
+ startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small" }) })),
155
+ endAdornment: filter ? (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { size: "small", "aria-label": "clear filter", onClick: () => {
156
+ setFilter('');
157
+ }, children: _jsx(ClearIcon, { fontSize: "small" }) }) })) : null,
158
+ },
159
+ } }), 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
160
  });
70
161
  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
  },
@@ -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;
@@ -0,0 +1,10 @@
1
+ /** #slotEditor numeric text field that rounds to an integer */
2
+ declare const IntegerEditor: ({ slot, }: {
3
+ slot: {
4
+ name?: string;
5
+ value: number;
6
+ description?: string;
7
+ set: (num: number) => void;
8
+ };
9
+ }) => import("react").JSX.Element;
10
+ export default IntegerEditor;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { observer } from 'mobx-react';
3
+ import NumberEditor from './NumberEditor.js';
4
+ // thin wrapper: an integer slot is a number slot that only commits whole values
5
+ /** #slotEditor numeric text field that rounds to an integer */
6
+ const IntegerEditor = observer(function IntegerEditor({ slot, }) {
7
+ return _jsx(NumberEditor, { slot: slot, integer: true });
8
+ });
9
+ export default IntegerEditor;
@@ -1,9 +1,11 @@
1
+ /** #slotEditor monospace textarea holding arbitrary JSON */
1
2
  declare const JsonEditor: ({ slot, }: {
2
3
  slot: {
3
4
  name: string;
4
5
  description: string;
5
6
  value: unknown;
7
+ promotedBase?: unknown;
6
8
  set: (arg: unknown) => void;
7
9
  };
8
- }) => import("react/jsx-runtime").JSX.Element;
10
+ }) => import("react").JSX.Element;
9
11
  export default JsonEditor;
@@ -1,53 +1,40 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { MonospaceTextField } from '@jbrowse/core/ui';
3
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
- import { InputLabel, TextField } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
- const fontSize = '12px';
7
- 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';
8
6
  const useStyles = makeStyles()(theme => ({
9
- error: {
10
- color: 'red',
11
- fontSize: '0.8em',
12
- },
13
- callbackEditor: {
14
- fontFamily,
15
- fontSize,
7
+ jsonEditor: {
8
+ fontSize: '12px',
16
9
  background: theme.palette.background.default,
17
- width: 800,
10
+ width: '100%',
18
11
  marginTop: '16px',
19
12
  border: '1px solid rgba(0,0,0,0.42)',
20
13
  },
21
- callbackContainer: {
22
- width: '100%',
23
- overflowX: 'auto',
24
- },
25
- textAreaFont: {
26
- fontFamily,
27
- },
28
14
  }));
15
+ /** #slotEditor monospace textarea holding arbitrary JSON */
29
16
  const JsonEditor = observer(function JsonEditor({ slot, }) {
30
17
  const { classes } = useStyles();
31
- const [contents, setContents] = useState(JSON.stringify(slot.value, null, 2));
18
+ // A `maybeFrozen` slot is `undefined` when unset — the promotable inherit
19
+ // state (alignments/synteny `colorBy`). `JSON.stringify(undefined)` returns
20
+ // the *value* `undefined`, not a string, so seeding straight off `slot.value`
21
+ // handed MonospaceTextField an undefined `value` and made the field
22
+ // uncontrolled until the first keystroke (React then warns on the switch).
23
+ // Show what unset resolves to instead, as BooleanEditor does.
24
+ const [contents, setContents] = useState(() => {
25
+ const shown = slot.value ?? slot.promotedBase;
26
+ return shown === undefined ? '' : JSON.stringify(shown, null, 2);
27
+ });
32
28
  const [error, setError] = useState();
33
- useEffect(() => {
34
- try {
35
- setError(undefined);
36
- slot.set(JSON.parse(contents));
37
- }
38
- catch (e) {
39
- console.error({ e });
40
- setError(e);
41
- }
42
- }, [contents, slot]);
43
- return (_jsxs(_Fragment, { children: [error ? _jsx("p", { className: classes.error, children: `${error}` }) : null, _jsxs("div", { className: classes.callbackContainer, children: [_jsx(InputLabel, { shrink: true, htmlFor: "json-editor", children: slot.name }), _jsx(TextField, { id: "json-editor", className: classes.callbackEditor, value: contents, helperText: slot.description, multiline: true, onChange: event => {
44
- setContents(event.target.value);
45
- }, style: { background: error ? '#fdd' : undefined }, slotProps: {
46
- input: {
47
- classes: {
48
- input: classes.textAreaFont,
49
- },
50
- },
51
- } })] })] }));
29
+ return (_jsx(MonospaceTextField, { className: classes.jsonEditor, label: slot.name, value: contents, error: error, helperText: slot.description, onChange: val => {
30
+ setContents(val);
31
+ try {
32
+ slot.set(JSON.parse(val));
33
+ setError(undefined);
34
+ }
35
+ catch (e) {
36
+ setError(e);
37
+ }
38
+ } }));
52
39
  });
53
40
  export default JsonEditor;
@@ -0,0 +1,8 @@
1
+ export default function MapEntryCard({ title, onDelete, children, }: {
2
+ title: string;
3
+ onDelete: () => void;
4
+ children: React.ReactNode;
5
+ }): import("react").JSX.Element;
6
+ export declare function MapAddCard({ onAdd }: {
7
+ onAdd: (key: string) => void;
8
+ }): import("react").JSX.Element;