@jbrowse/plugin-config 4.3.0 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
3
+ import DeleteIcon from '@mui/icons-material/Delete';
4
+ import { Card, CardContent, CardHeader, IconButton } from '@mui/material';
5
+ import AddNewField from './AddNewField.js';
6
+ const useStyles = makeStyles()(theme => ({
7
+ card: {
8
+ marginTop: theme.spacing(1),
9
+ },
10
+ }));
11
+ // shared card for a single key in a map slot editor: a titled card with a
12
+ // delete button wrapping the per-value editor passed as children
13
+ export default function MapEntryCard({ title, onDelete, children, }) {
14
+ const { classes } = useStyles();
15
+ return (_jsxs(Card, { raised: true, className: classes.card, children: [_jsx(CardHeader, { title: title, action: _jsx(IconButton, { onClick: () => {
16
+ onDelete();
17
+ }, children: _jsx(DeleteIcon, {}) }) }), _jsx(CardContent, { children: children })] }));
18
+ }
19
+ // trailing card in a map slot editor holding the "add new key" field
20
+ export function MapAddCard({ onAdd }) {
21
+ const { classes } = useStyles();
22
+ return (_jsx(Card, { raised: true, className: classes.card, children: _jsx(CardHeader, { disableTypography: true, title: _jsx(AddNewField, { onAdd: onAdd }) }) }));
23
+ }
@@ -0,0 +1,8 @@
1
+ export default function MapSlotEditor<V>({ name, description, entries, emptyValue, setMap, renderValue, }: {
2
+ name: string;
3
+ description: string;
4
+ entries: [string, V][];
5
+ emptyValue: V;
6
+ setMap: (val: Record<string, V>) => void;
7
+ renderValue: (val: V, set: (val: V) => void, key: string) => React.ReactNode;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FormHelperText, InputLabel } from '@mui/material';
3
+ import MapEntryCard, { MapAddCard } from './MapEntryCard.js';
4
+ // shared shell for map slot editors (numberMap, stringArrayMap): a labelled
5
+ // list of per-key cards plus an "add key" card. The owning wrapper stays the
6
+ // observer and passes plain entries in, so this stays a non-observer generic
7
+ // (which keeps JSX type args working at the call sites).
8
+ export default function MapSlotEditor({ name, description, entries, emptyValue, setMap, renderValue, }) {
9
+ const obj = Object.fromEntries(entries);
10
+ return (_jsxs(_Fragment, { children: [_jsx(InputLabel, { children: name }), entries.map(([key, val]) => (_jsx(MapEntryCard, { title: key, onDelete: () => {
11
+ const { [key]: _omit, ...rest } = obj;
12
+ setMap(rest);
13
+ }, children: renderValue(val, newVal => {
14
+ setMap({ ...obj, [key]: newVal });
15
+ }, key) }, key))), _jsx(MapAddCard, { onAdd: key => {
16
+ setMap({ ...obj, [key]: emptyValue });
17
+ } }), _jsx(FormHelperText, { children: description })] }));
18
+ }
@@ -1,10 +1,12 @@
1
- declare const NumberEditor: ({ slot, }: {
1
+ /** #slotEditor numeric text field */
2
+ declare const NumberEditor: ({ slot, integer, }: {
2
3
  slot: {
3
4
  name?: string;
4
- value: string;
5
+ value: number | undefined;
5
6
  description?: string;
7
+ promotedBase?: unknown;
6
8
  set: (val: number) => void;
7
- reset?: () => void;
8
9
  };
9
- }) => import("react/jsx-runtime").JSX.Element;
10
+ integer?: boolean;
11
+ }) => import("react").JSX.Element;
10
12
  export default NumberEditor;
@@ -1,20 +1,29 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useState } from 'react';
3
3
  import { observer } from 'mobx-react';
4
- import ConfigurationTextField from "./ConfigurationTextField.js";
5
- const NumberEditor = observer(function NumberEditor({ slot, }) {
6
- const [val, setVal] = useState(slot.value);
7
- useEffect(() => {
8
- const num = Number.parseFloat(val);
9
- if (Number.isNaN(num)) {
10
- slot.reset?.();
11
- }
12
- else {
13
- slot.set(num);
14
- }
15
- }, [slot, val]);
16
- return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: val, type: "number", onChange: evt => {
17
- setVal(evt.target.value);
4
+ import ConfigurationTextField from './ConfigurationTextField.js';
5
+ /** #slotEditor numeric text field */
6
+ const NumberEditor = observer(function NumberEditor({ slot, integer = false, }) {
7
+ const [val, setVal] = useState(slot.value === undefined ? '' : String(slot.value));
8
+ // An unset `maybeNumber` is a promotable slot's inherit sentinel, and an
9
+ // empty box is the honest rendering of it — a number field, unlike a
10
+ // checkbox, has a state for "nothing". What it did not say is what the track
11
+ // is therefore drawing at, so `featureHeight` read as a blank with no clue
12
+ // that reads are 7px tall. The placeholder is that, in the affordance meant
13
+ // for it; the label has to float for MUI to draw one over a labelled field.
14
+ const placeholder = slot.value === undefined && slot.promotedBase !== undefined
15
+ ? String(slot.promotedBase)
16
+ : undefined;
17
+ return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, placeholder: placeholder, slotProps: placeholder ? { inputLabel: { shrink: true } } : undefined, value: val, onChange: evt => {
18
+ const v = evt.target.value;
19
+ setVal(v);
20
+ const num = Number(v);
21
+ // commit only valid values; the text buffer preserves in-progress
22
+ // entries like "-" or "1." without writing garbage to the config
23
+ const valid = integer ? Number.isInteger(num) : Number.isFinite(num);
24
+ if (v !== '' && valid) {
25
+ slot.set(num);
26
+ }
18
27
  } }));
19
28
  });
20
29
  export default NumberEditor;
@@ -1,10 +1,10 @@
1
+ /** #slotEditor one card per key, each holding that key's numeric field */
1
2
  declare const NumberMapEditor: ({ slot, }: {
2
3
  slot: {
3
4
  name: string;
4
- value: Map<string, string>;
5
- remove: (key: string) => void;
6
- add: (key: string, val: number) => void;
5
+ value: Map<string, number>;
6
+ set: (val: Record<string, number>) => void;
7
7
  description: string;
8
8
  };
9
- }) => import("react/jsx-runtime").JSX.Element;
9
+ }) => import("react").JSX.Element;
10
10
  export default NumberMapEditor;
@@ -1,35 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { makeStyles } from '@jbrowse/core/util/tss-react';
4
- import AddIcon from '@mui/icons-material/Add';
5
- import DeleteIcon from '@mui/icons-material/Delete';
6
- import { Card, CardContent, CardHeader, FormHelperText, IconButton, InputAdornment, InputLabel, TextField, } from '@mui/material';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
7
2
  import { observer } from 'mobx-react';
8
- import NumberEditor from "./NumberEditor.js";
9
- const useStyles = makeStyles()(theme => ({
10
- card: {
11
- marginTop: theme.spacing(1),
12
- },
13
- }));
3
+ import MapSlotEditor from './MapSlotEditor.js';
4
+ import NumberEditor from './NumberEditor.js';
5
+ /** #slotEditor one card per key, each holding that key's numeric field */
14
6
  const NumberMapEditor = observer(function NumberMapEditor({ slot, }) {
15
- const { classes } = useStyles();
16
- const [value, setValue] = useState('');
17
- return (_jsxs(_Fragment, { children: [_jsx(InputLabel, { children: slot.name }), [...slot.value].map(([key, val]) => (_jsxs(Card, { raised: true, className: classes.card, children: [_jsx(CardHeader, { title: key, action: _jsx(IconButton, { onClick: () => {
18
- slot.remove(key);
19
- }, children: _jsx(DeleteIcon, {}) }) }), _jsx(CardContent, { children: _jsx(NumberEditor, { slot: {
20
- value: val,
21
- set: (val) => {
22
- slot.add(key, val);
23
- },
24
- } }) })] }, key))), _jsx(Card, { raised: true, className: classes.card, children: _jsx(CardHeader, { disableTypography: true, title: _jsx(TextField, { fullWidth: true, value: value, placeholder: "add new", onChange: event => {
25
- setValue(event.target.value);
26
- }, slotProps: {
27
- input: {
28
- endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { disabled: value === '', onClick: () => {
29
- slot.add(value, 0);
30
- setValue('');
31
- }, children: _jsx(AddIcon, {}) }) })),
32
- },
33
- } }) }) }), _jsx(FormHelperText, { children: slot.description })] }));
7
+ return (_jsx(MapSlotEditor, { name: slot.name, description: slot.description, entries: [...slot.value], emptyValue: 0, setMap: val => {
8
+ slot.set(val);
9
+ }, renderValue: (val, set) => _jsx(NumberEditor, { slot: { value: val, set } }) }));
34
10
  });
35
11
  export default NumberMapEditor;
@@ -1,6 +1,5 @@
1
- import type { IAnyType } from '@jbrowse/mobx-state-tree';
2
- declare const SlotEditor: ({ slot, slotSchema, }: {
3
- slot: any;
4
- slotSchema: IAnyType;
5
- }) => import("react/jsx-runtime").JSX.Element;
1
+ import type { SlotFacade } from '@jbrowse/core/configuration';
2
+ declare const SlotEditor: ({ slot, }: {
3
+ slot: SlotFacade;
4
+ }) => import("react").JSX.Element;
6
5
  export default SlotEditor;
@@ -1,57 +1,65 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useState } from 'react';
3
+ import { isCallbackValue, toCallbackValue, toFixedValue, } from '@jbrowse/core/configuration';
3
4
  import { FileSelector } from '@jbrowse/core/ui';
4
- import { getEnv } from '@jbrowse/core/util';
5
- import { getSubType, getUnionSubTypes } from '@jbrowse/core/util/mst-reflection';
6
- import { getPropertyMembers } from '@jbrowse/mobx-state-tree';
7
- import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
8
- import { IconButton, MenuItem, Paper, SvgIcon, TextField } from '@mui/material';
5
+ import CodeIcon from '@mui/icons-material/Code';
6
+ import RestartAltIcon from '@mui/icons-material/RestartAlt';
7
+ import { IconButton, MenuItem, Paper, TextField, Tooltip } from '@mui/material';
9
8
  import { observer } from 'mobx-react';
10
- import BooleanEditor from "./BooleanEditor.js";
11
- import CallbackEditor from "./CallbackEditor.js";
12
- import ColorEditor from "./ColorEditor.js";
13
- import ConfigurationTextField from "./ConfigurationTextField.js";
14
- import JsonEditor from "./JsonEditor.js";
15
- import NumberEditor from "./NumberEditor.js";
16
- import NumberMapEditor from "./NumberMapEditor.js";
17
- import StringArrayEditor from "./StringArrayEditor.js";
18
- import StringArrayMapEditor from "./StringArrayMapEditor.js";
19
- import { useSlotEditorStyles } from "./useSlotEditorStyles.js";
9
+ import BooleanEditor from './BooleanEditor.js';
10
+ import CallbackEditor from './CallbackEditor.js';
11
+ import ColorEditor from './ColorEditor.js';
12
+ import ConfigurationTextField from './ConfigurationTextField.js';
13
+ import IntegerEditor from './IntegerEditor.js';
14
+ import JsonEditor from './JsonEditor.js';
15
+ import NumberEditor from './NumberEditor.js';
16
+ import NumberMapEditor from './NumberMapEditor.js';
17
+ import StringArrayEditor from './StringArrayEditor.js';
18
+ import StringArrayMapEditor from './StringArrayMapEditor.js';
19
+ import { useSlotEditorStyles } from './useSlotEditorStyles.js';
20
+ /** #slotEditor single-line text field */
20
21
  const StringEditor = observer(function StringEditor({ slot, }) {
21
22
  return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: slot.value, onChange: evt => {
22
23
  slot.set(evt.target.value);
23
24
  } }));
24
25
  });
26
+ /** #slotEditor multi-line textarea */
25
27
  const TextEditor = observer(function TextEditor({ slot, }) {
26
28
  return (_jsx(TextField, { label: slot.name, helperText: slot.description, multiline: true, value: slot.value, onChange: evt => {
27
29
  slot.set(evt.target.value);
28
30
  } }));
29
31
  });
30
- const SvgCheckbox = () => (_jsx(SvgIcon, { children: _jsx("path", { d: "M20.41,3C21.8,5.71 22.35,8.84 22,12C21.8,15.16 20.7,18.29 18.83,21L17.3,20C18.91,17.57 19.85,14.8 20,12C20.34,9.2 19.89,6.43 18.7,4L20.41,3M5.17,3L6.7,4C5.09,6.43 4.15,9.2 4,12C3.66,14.8 4.12,17.57 5.3,20L3.61,21C2.21,18.29 1.65,15.17 2,12C2.2,8.84 3.3,5.71 5.17,3M12.08,10.68L14.4,7.45H16.93L13.15,12.45L15.35,17.37H13.09L11.71,14L9.28,17.33H6.76L10.66,12.21L8.53,7.45H10.8L12.08,10.68Z" }) }));
31
- const IntegerEditor = observer(function IntegerEditor({ slot, }) {
32
- const [val, setVal] = useState(slot.value);
33
- useEffect(() => {
34
- const num = Number.parseInt(val, 10);
35
- if (!Number.isNaN(num)) {
36
- slot.set(num);
37
- }
38
- }, [slot, val]);
39
- return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: val, type: "number", onChange: evt => {
40
- setVal(evt.target.value);
41
- } }));
42
- });
43
- const StringEnumEditor = observer(function StringEnumEditor({ slot, slotSchema, }) {
44
- const p = getPropertyMembers(getSubType(slotSchema));
45
- const choices = getUnionSubTypes(getUnionSubTypes(getSubType(p.properties.value))[1]).map(t => t.value);
46
- return (_jsx(ConfigurationTextField, { value: slot.value, label: slot.name, select: true, helperText: slot.description, onChange: evt => {
47
- slot.set(evt.target.value);
48
- }, children: choices.map(str => (_jsx(MenuItem, { value: str, children: str }, str))) }));
32
+ // Sentinel for "leave the slot unset" in the maybe-enum dropdown. MUI needs a
33
+ // concrete option value, and '' can't collide with an enum member (an empty
34
+ // enumeration member would be unnameable in a menu anyway).
35
+ const UNSET_CHOICE = '';
36
+ /** #slotEditor dropdown of the `model`'s members */
37
+ const StringEnumEditor = observer(function StringEnumEditor({ slot, }) {
38
+ const nullable = slot.type === 'maybeStringEnum';
39
+ // Naming the member unset resolves to, rather than a bare "default": every
40
+ // other choice in the list is a value the user can read off the plot, and
41
+ // this one was the only one that said nothing about what picking it draws.
42
+ // `promotedBase` is the honest half to show — the editor's target is often a
43
+ // detached config node, so it cannot see a value promoted this session.
44
+ const unsetLabel = typeof slot.promotedBase === 'string'
45
+ ? `default (${slot.promotedBase})`
46
+ : 'default';
47
+ return (_jsxs(ConfigurationTextField, { value: slot.value ?? UNSET_CHOICE, label: slot.name, select: true, helperText: slot.description, onChange: evt => {
48
+ const { value } = evt.target;
49
+ slot.set(value === UNSET_CHOICE ? undefined : value);
50
+ }, children: [nullable ? (_jsx(MenuItem, { value: UNSET_CHOICE, children: _jsx("em", { children: unsetLabel }) })) : null, (slot.choices ?? []).map(str => (_jsx(MenuItem, { value: str, children: str }, str)))] }));
49
51
  });
52
+ /** #slotEditor URL, local file path (desktop) or file blob (browser) */
50
53
  const FileSelectorWrapper = observer(function FileSelectorWrapper({ slot, }) {
51
54
  return (_jsx(FileSelector, { location: slot.value, setLocation: location => {
52
55
  slot.set(location);
53
- }, name: slot.name, description: slot.description, rootModel: getEnv(slot).pluginManager.rootModel }));
56
+ }, name: slot.name, description: slot.description, rootModel: slot.pluginManager.rootModel }));
54
57
  });
58
+ // dynamic dispatch from a runtime slot `type` string to a typed editor. The
59
+ // `any` is irreducible here: each editor declares a narrow `slot.value` type
60
+ // (number, boolean, FileLocation, string[], ...) but makeSlotFacade can only
61
+ // type value as `unknown`, so no single registry prop type satisfies them all.
62
+ // The editors stay fully typed internally; only this lookup is untyped.
55
63
  const valueComponents = {
56
64
  string: StringEditor,
57
65
  text: TextEditor,
@@ -60,27 +68,54 @@ const valueComponents = {
60
68
  stringArrayMap: StringArrayMapEditor,
61
69
  numberMap: NumberMapEditor,
62
70
  number: NumberEditor,
71
+ maybeNumber: NumberEditor,
63
72
  integer: IntegerEditor,
64
73
  color: ColorEditor,
74
+ maybeColor: ColorEditor,
65
75
  stringEnum: StringEnumEditor,
76
+ maybeStringEnum: StringEnumEditor,
66
77
  boolean: BooleanEditor,
78
+ maybeBoolean: BooleanEditor,
67
79
  frozen: JsonEditor,
68
- configRelationships: JsonEditor,
80
+ maybeFrozen: JsonEditor,
69
81
  };
70
- const SlotEditor = observer(function SlotEditor({ slot, slotSchema, }) {
82
+ const SlotEditor = observer(function SlotEditor({ slot, }) {
71
83
  const { classes } = useSlotEditorStyles();
72
84
  const { type } = slot;
73
- let ValueComponent = slot.isCallback
74
- ? CallbackEditor
75
- :
76
- valueComponents[type];
77
- if (!ValueComponent) {
85
+ // editor mode is UI-only state, derived once from whether the stored value is
86
+ // a jexl callback. Toggling rewrites the value; typing a "jexl:" prefix into a
87
+ // value field does not auto-swap the editor, since this state only changes on
88
+ // the explicit toggle.
89
+ const [callbackMode, setCallbackMode] = useState(() => isCallbackValue(slot.value));
90
+ // bumped on reset to remount the value editor: the buffered editors (number,
91
+ // json, callback) seed internal state from slot.value only on mount, so an
92
+ // external set wouldn't otherwise show in the field
93
+ const [resetNonce, setResetNonce] = useState(0);
94
+ const TypedComponent = valueComponents[type];
95
+ if (!callbackMode && !TypedComponent) {
78
96
  console.warn(`no slot editor defined for ${type}, editing as string`);
79
- ValueComponent = StringEditor;
80
- }
81
- if (!(type in valueComponents)) {
82
- console.warn(`SlotEditor needs to implement ${type}`);
83
97
  }
84
- return (_jsxs(Paper, { className: classes.paper, children: [_jsx("div", { className: classes.paperContent, children: _jsx(ValueComponent, { slot: slot, slotSchema: slotSchema }) }), _jsx("div", { className: classes.slotModeSwitch, children: slot.contextVariable.length ? (_jsx(IconButton, { onClick: () => slot.isCallback ? slot.convertToValue() : slot.convertToCallback(), title: `convert to ${slot.isCallback ? 'regular value' : 'callback'}`, children: slot.isCallback ? _jsx(SvgCheckbox, {}) : _jsx(RadioButtonUncheckedIcon, {}) })) : null })] }));
98
+ const ValueComponent = callbackMode
99
+ ? CallbackEditor
100
+ : (TypedComponent ?? StringEditor);
101
+ // a promotable slot's default is its inherit sentinel, so resetting it here
102
+ // doubles as "un-pin / follow the session-wide default"
103
+ const { modified } = slot;
104
+ return (_jsxs(Paper, { className: classes.paper, children: [_jsx("div", { className: classes.paperContent, children: _jsx(ValueComponent, { slot: slot }, resetNonce) }), modified ? (_jsx("div", { className: classes.resetButton, children: _jsx(Tooltip, { title: "Reset to default", children: _jsx(IconButton, { "aria-label": "reset to default", onClick: () => {
105
+ slot.set(slot.defaultValue);
106
+ setCallbackMode(isCallbackValue(slot.defaultValue));
107
+ setResetNonce(nonce => nonce + 1);
108
+ }, children: _jsx(RestartAltIcon, {}) }) }) })) : null, slot.contextVariable.length ? (_jsx("div", { className: classes.slotModeSwitch, children: _jsx(Tooltip, { title: callbackMode
109
+ ? 'Editing as a callback (Jexl expression). Click to use a fixed value.'
110
+ : 'Editing as a fixed value. Click to use a callback (Jexl expression).', children: _jsx(IconButton, { color: callbackMode ? 'primary' : 'default', onClick: () => {
111
+ if (callbackMode) {
112
+ slot.set(toFixedValue(slot.value, type, slot.defaultValue, slot.pluginManager.jexl));
113
+ setCallbackMode(false);
114
+ }
115
+ else {
116
+ slot.set(toCallbackValue(slot.value));
117
+ setCallbackMode(true);
118
+ }
119
+ }, children: _jsx(CodeIcon, {}) }) }) })) : null] }));
85
120
  });
86
121
  export default SlotEditor;
@@ -1,11 +1,10 @@
1
+ /** #slotEditor "todolist" of text fields, one per entry, with add and delete */
1
2
  declare const StringArrayEditor: ({ slot, }: {
2
3
  slot: {
3
4
  name: string;
4
5
  value: string[];
5
- setAtIndex: (arg: number, arg2: string) => void;
6
- removeAtIndex: (arg: number) => void;
7
- add: (arg: string) => void;
6
+ set: (arg: string[]) => void;
8
7
  description: string;
9
8
  };
10
- }) => import("react/jsx-runtime").JSX.Element;
9
+ }) => import("react").JSX.Element;
11
10
  export default StringArrayEditor;
@@ -1,34 +1,21 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
2
  import DeleteIcon from '@mui/icons-material/Delete';
4
- import { Button, FormHelperText, IconButton, InputAdornment, InputLabel, List, ListItem, TextField, } from '@mui/material';
3
+ import { FormHelperText, IconButton, InputAdornment, InputLabel, List, ListItem, TextField, } from '@mui/material';
5
4
  import { observer } from 'mobx-react';
5
+ import AddNewField from './AddNewField.js';
6
+ /** #slotEditor "todolist" of text fields, one per entry, with add and delete */
6
7
  const StringArrayEditor = observer(function StringArrayEditor({ slot, }) {
7
- const [value, setValue] = useState('');
8
- const [addNew, setAddNew] = useState(false);
9
- return (_jsxs(_Fragment, { children: [slot.name ? _jsx(InputLabel, { children: slot.name }) : null, _jsxs(List, { disablePadding: true, children: [slot.value.map((val, idx) => (_jsx(ListItem, { disableGutters: true, children: _jsx(TextField, { value: val, onChange: evt => {
10
- slot.setAtIndex(idx, evt.target.value);
8
+ const value = [...slot.value];
9
+ return (_jsxs(_Fragment, { children: [slot.name ? _jsx(InputLabel, { children: slot.name }) : null, _jsxs(List, { disablePadding: true, children: [value.map((val, idx) => (_jsx(ListItem, { disableGutters: true, children: _jsx(TextField, { fullWidth: true, value: val, onChange: evt => {
10
+ slot.set(value.map((v, i) => (i === idx ? evt.target.value : v)));
11
11
  }, slotProps: {
12
12
  input: {
13
13
  endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
14
- slot.removeAtIndex(idx);
14
+ slot.set(value.filter((_, i) => i !== idx));
15
15
  }, children: _jsx(DeleteIcon, {}) }) })),
16
16
  },
17
- } }) }, `${JSON.stringify(val)}-${idx}`))), addNew ? (_jsx(ListItem, { disableGutters: true, children: _jsx(TextField, { value: value, placeholder: "add new", onChange: event => {
18
- setValue(event.target.value);
19
- }, slotProps: {
20
- input: {
21
- endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsxs(_Fragment, { children: [_jsx(Button, { color: "primary", variant: "contained", style: { margin: 2 }, "data-testid": `stringArrayAdd-${slot.name}`, onClick: () => {
22
- setAddNew(false);
23
- slot.add(value);
24
- setValue('');
25
- }, children: "OK" }), _jsx(Button, { color: "primary", variant: "contained", style: { margin: 2 }, onClick: () => {
26
- setAddNew(false);
27
- setValue('');
28
- }, children: "Cancel" })] }) })),
29
- },
30
- } }) })) : null, _jsx(Button, { color: "primary", variant: "contained", style: { margin: 4 }, disabled: addNew, onClick: () => {
31
- setAddNew(true);
32
- }, children: "Add item" })] }), _jsx(FormHelperText, { children: slot.description })] }));
17
+ } }) }, idx))), _jsx(ListItem, { disableGutters: true, children: _jsx(AddNewField, { testid: `stringArrayAdd-${slot.name}`, onAdd: val => {
18
+ slot.set([...value, val]);
19
+ } }) })] }), _jsx(FormHelperText, { children: slot.description })] }));
33
20
  });
34
21
  export default StringArrayEditor;
@@ -1,13 +1,10 @@
1
+ /** #slotEditor one card per key, each holding that key's "todolist" of strings */
1
2
  declare const StringArrayMapEditor: ({ slot, }: {
2
3
  slot: {
3
4
  name: string;
4
5
  value: Map<string, string[]>;
5
- remove: (key: string) => void;
6
- add: (key: string, val: string[]) => void;
6
+ set: (val: Record<string, string[]>) => void;
7
7
  description: string;
8
- setAtKeyIndex: (key: string, idx: number, val: string) => void;
9
- removeAtKeyIndex: (key: string, idx: number) => void;
10
- addToKey: (key: string, val: string) => void;
11
8
  };
12
- }) => import("react/jsx-runtime").JSX.Element;
9
+ }) => import("react").JSX.Element;
13
10
  export default StringArrayMapEditor;
@@ -1,43 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { makeStyles } from '@jbrowse/core/util/tss-react';
4
- import AddIcon from '@mui/icons-material/Add';
5
- import DeleteIcon from '@mui/icons-material/Delete';
6
- import { Card, CardContent, CardHeader, FormHelperText, IconButton, InputAdornment, InputLabel, TextField, } from '@mui/material';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
7
2
  import { observer } from 'mobx-react';
8
- import StringArrayEditor from "./StringArrayEditor.js";
9
- const useStyles = makeStyles()(theme => ({
10
- card: {
11
- marginTop: theme.spacing(1),
12
- },
13
- }));
3
+ import MapSlotEditor from './MapSlotEditor.js';
4
+ import StringArrayEditor from './StringArrayEditor.js';
5
+ /** #slotEditor one card per key, each holding that key's "todolist" of strings */
14
6
  const StringArrayMapEditor = observer(function StringArrayMapEditor({ slot, }) {
15
- const { classes } = useStyles();
16
- const [value, setValue] = useState('');
17
- return (_jsxs(_Fragment, { children: [_jsx(InputLabel, { children: slot.name }), [...slot.value].map(([key, val]) => (_jsxs(Card, { raised: true, className: classes.card, children: [_jsx(CardHeader, { title: key, action: _jsx(IconButton, { onClick: () => {
18
- slot.remove(key);
19
- }, children: _jsx(DeleteIcon, {}) }) }), _jsx(CardContent, { children: _jsx(StringArrayEditor, { slot: {
20
- name: slot.name,
21
- value: val,
22
- description: `Values associated with entry ${key}`,
23
- setAtIndex: (idx, val) => {
24
- slot.setAtKeyIndex(key, idx, val);
25
- },
26
- removeAtIndex: (idx) => {
27
- slot.removeAtKeyIndex(key, idx);
28
- },
29
- add: (val) => {
30
- slot.addToKey(key, val);
31
- },
32
- } }) })] }, key))), _jsx(Card, { raised: true, className: classes.card, children: _jsx(CardHeader, { disableTypography: true, title: _jsx(TextField, { fullWidth: true, value: value, placeholder: "add new", onChange: event => {
33
- setValue(event.target.value);
34
- }, slotProps: {
35
- input: {
36
- endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { disabled: value === '', onClick: () => {
37
- slot.add(value, []);
38
- setValue('');
39
- }, children: _jsx(AddIcon, {}) }) })),
40
- },
41
- } }) }) }), _jsx(FormHelperText, { children: slot.description })] }));
7
+ return (_jsx(MapSlotEditor, { name: slot.name, description: slot.description,
8
+ // deep copy so per-entry edits never reuse the live MST array nodes
9
+ entries: [...slot.value].map(([k, v]) => [k, [...v]]), emptyValue: [], setMap: val => {
10
+ slot.set(val);
11
+ }, renderValue: (val, set, key) => (_jsx(StringArrayEditor, { slot: {
12
+ name: '',
13
+ value: val,
14
+ description: `Values associated with entry ${key}`,
15
+ set,
16
+ } })) }));
42
17
  });
43
18
  export default StringArrayMapEditor;
@@ -4,5 +4,5 @@ declare const TypeSelector: ({ typeNameChoices, slot, slotName, onChange, }: {
4
4
  slot: AnyConfigurationModel;
5
5
  slotName: string;
6
6
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
7
- }) => import("react/jsx-runtime").JSX.Element;
7
+ }) => import("react").JSX.Element;
8
8
  export default TypeSelector;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { MenuItem, Paper, TextField } from '@mui/material';
3
3
  import { observer } from 'mobx-react';
4
- import { useSlotEditorStyles } from "./useSlotEditorStyles.js";
4
+ import { useSlotEditorStyles } from './useSlotEditorStyles.js';
5
5
  const TypeSelector = observer(function TypeSelector({ typeNameChoices, slot, slotName, onChange, }) {
6
6
  const { classes } = useSlotEditorStyles();
7
- return (_jsx(Paper, { className: classes.paper, children: _jsx("div", { className: classes.paperContent, children: _jsx(TextField, { value: slot.type, label: "Type", select: true, helperText: `Type of ${slotName} to use`, fullWidth: true, onChange: onChange, children: typeNameChoices.map(str => (_jsx(MenuItem, { value: str, children: str }, str))) }) }) }));
7
+ return (_jsx(Paper, { className: classes.paper, children: _jsx("div", { className: classes.paperContent, children: _jsx(TextField, { value: slot.type, label: "Type", select: true, variant: "outlined", helperText: `Type of ${slotName} to use`, fullWidth: true, onChange: onChange, children: typeNameChoices.map(str => (_jsx(MenuItem, { value: str, children: str }, str))) }) }) }));
8
8
  });
9
9
  export default TypeSelector;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Singularize a config schema slot name for the per-entry label an array-typed
3
+ * sub-schema renders ("displays" -> "display LinearBasicDisplay").
4
+ *
5
+ * Slot names are developer-authored camelCase identifiers — `displays`,
6
+ * `tracks`, `connections` are the ones in tree — so the regular English endings
7
+ * are the whole domain. This replaced the `pluralize` dependency, whose value
8
+ * is its irregular-noun table, and nothing here is ever an irregular noun.
9
+ *
10
+ * The counterpart for label text, where the noun comes from a display rather
11
+ * than a schema key, is core's own `pluralize(count, noun)` in `stringUtils`.
12
+ */
13
+ export declare function singularSlotName(name: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Singularize a config schema slot name for the per-entry label an array-typed
3
+ * sub-schema renders ("displays" -> "display LinearBasicDisplay").
4
+ *
5
+ * Slot names are developer-authored camelCase identifiers — `displays`,
6
+ * `tracks`, `connections` are the ones in tree — so the regular English endings
7
+ * are the whole domain. This replaced the `pluralize` dependency, whose value
8
+ * is its irregular-noun table, and nothing here is ever an irregular noun.
9
+ *
10
+ * The counterpart for label text, where the noun comes from a display rather
11
+ * than a schema key, is core's own `pluralize(count, noun)` in `stringUtils`.
12
+ */
13
+ export function singularSlotName(name) {
14
+ return /[^aeiou]ies$/.test(name)
15
+ ? `${name.slice(0, -3)}y`
16
+ : /(?:s|sh|ch|x|z)es$/.test(name)
17
+ ? name.slice(0, -2)
18
+ : // a trailing `s` after `a`/`i`/`u`/`s` belongs to a word that is already
19
+ // singular — `bias`, `alias`, `axis`, `status`, `class` — not to a plural
20
+ /[^saiu]s$/.test(name)
21
+ ? name.slice(0, -1)
22
+ : name;
23
+ }
@@ -3,8 +3,8 @@ export declare const useSlotEditorStyles: (_params?: unknown, muiStyleOverridesP
3
3
  classes?: Record<string, string>;
4
4
  };
5
5
  } | undefined) => {
6
- classes: Record<"paper" | "paperContent" | "slotModeSwitch", string>;
7
- theme: import("@mui/material").Theme;
6
+ classes: Record<"paper" | "paperContent" | "resetButton" | "slotModeSwitch", string>;
7
+ theme: import("@jbrowse/core/ui/styleTheme").JBrowseStyleTheme;
8
8
  css: import("@jbrowse/core/util/tss-react/types").Css;
9
9
  cx: import("@jbrowse/core/util/tss-react/types").Cx;
10
10
  };
@@ -6,7 +6,8 @@ export const useSlotEditorStyles = makeStyles()(theme => ({
6
6
  position: 'relative',
7
7
  },
8
8
  paperContent: {
9
- width: '100%',
9
+ flex: 1,
10
+ minWidth: 0,
10
11
  },
11
12
  slotModeSwitch: {
12
13
  width: 24,
@@ -15,4 +16,8 @@ export const useSlotEditorStyles = makeStyles()(theme => ({
15
16
  justifyContent: 'center',
16
17
  alignItems: 'center',
17
18
  },
19
+ resetButton: {
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ },
18
23
  }));
@@ -1,10 +1,10 @@
1
1
  import { lazy } from 'react';
2
2
  import { ConfigurationSchema } from '@jbrowse/core/configuration';
3
3
  import { WidgetType } from '@jbrowse/core/pluggableElementTypes';
4
- import HeadingComponent from "./components/HeadingComponent.js";
5
- import stateModelFactory from "./model.js";
4
+ import HeadingComponent from './components/HeadingComponent.js';
5
+ import stateModelFactory from './model.js';
6
6
  const configSchema = ConfigurationSchema('ConfigurationEditorWidget', {});
7
- const LazyConfigurationEditorComponent = lazy(() => import("./components/ConfigurationEditor.js"));
7
+ const LazyConfigurationEditorComponent = lazy(() => import('./components/ConfigurationEditor.js'));
8
8
  export default function registerConfigurationEditorWidget(pluginManager) {
9
9
  pluginManager.addWidgetType(() => {
10
10
  return new WidgetType({