@jbrowse/plugin-config 5.0.0-beta.1 → 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 (33) hide show
  1. package/esm/ConfigurationEditorWidget/components/AddNewField.d.ts +3 -1
  2. package/esm/ConfigurationEditorWidget/components/AddNewField.js +2 -1
  3. package/esm/ConfigurationEditorWidget/components/BooleanEditor.d.ts +0 -1
  4. package/esm/ConfigurationEditorWidget/components/BooleanEditor.js +1 -14
  5. package/esm/ConfigurationEditorWidget/components/ColorEditor.d.ts +2 -1
  6. package/esm/ConfigurationEditorWidget/components/ColorEditor.js +25 -8
  7. package/esm/ConfigurationEditorWidget/components/ConfigurationEditor.js +69 -21
  8. package/esm/ConfigurationEditorWidget/components/EntryListEditor.d.ts +17 -0
  9. package/esm/ConfigurationEditorWidget/components/EntryListEditor.js +35 -0
  10. package/esm/ConfigurationEditorWidget/components/JsonEditor.d.ts +0 -1
  11. package/esm/ConfigurationEditorWidget/components/JsonEditor.js +7 -11
  12. package/esm/ConfigurationEditorWidget/components/NumberEditor.d.ts +0 -1
  13. package/esm/ConfigurationEditorWidget/components/NumberEditor.js +1 -10
  14. package/esm/ConfigurationEditorWidget/components/SlotEditor.js +21 -17
  15. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.d.ts +14 -7
  16. package/esm/ConfigurationEditorWidget/components/StringArrayEditor.js +15 -5
  17. package/esm/ConfigurationEditorWidget/components/StringMapEditor.d.ts +10 -0
  18. package/esm/ConfigurationEditorWidget/components/StringMapEditor.js +13 -0
  19. package/esm/ConfigurationEditorWidget/components/TypeSelector.d.ts +1 -1
  20. package/esm/ConfigurationEditorWidget/components/TypeSelector.js +1 -1
  21. package/esm/ConfigurationEditorWidget/index.js +2 -2
  22. package/esm/FromConfigAdapter/configSchema.d.ts +12 -17
  23. package/esm/FromConfigRegionsAdapter/configSchema.d.ts +12 -17
  24. package/esm/FromConfigSequenceAdapter/configSchema.d.ts +12 -17
  25. package/esm/NcbiSequenceReportAliasAdapter/configSchema.d.ts +11 -20
  26. package/esm/NcbiSequenceReportAliasAdapter/configSchema.js +5 -8
  27. package/esm/RefNameAliasAdapter/configSchema.d.ts +14 -31
  28. package/esm/RefNameAliasAdapter/configSchema.js +6 -9
  29. package/esm/aliasUtils.d.ts +1 -1
  30. package/esm/aliasUtils.js +1 -8
  31. package/package.json +6 -6
  32. package/esm/normalizeUriSnapshot.d.ts +0 -1
  33. package/esm/normalizeUriSnapshot.js +0 -13
@@ -1,4 +1,6 @@
1
- export default function AddNewField({ onAdd, testid, }: {
1
+ import type { ReactNode } from 'react';
2
+ export default function AddNewField({ onAdd, testid, startAdornment, }: {
2
3
  onAdd: (value: string) => void;
3
4
  testid?: string;
5
+ startAdornment?: ReactNode;
4
6
  }): import("react").JSX.Element;
@@ -4,7 +4,7 @@ import AddIcon from '@mui/icons-material/Add';
4
4
  import { IconButton, InputAdornment, TextField } from '@mui/material';
5
5
  // shared "add new" entry field used by the string-array and map slot editors:
6
6
  // a text box that commits its contents via onAdd and clears itself
7
- export default function AddNewField({ onAdd, testid, }) {
7
+ export default function AddNewField({ onAdd, testid, startAdornment, }) {
8
8
  const [value, setValue] = useState('');
9
9
  function commit() {
10
10
  if (value !== '') {
@@ -20,6 +20,7 @@ export default function AddNewField({ onAdd, testid, }) {
20
20
  }
21
21
  }, slotProps: {
22
22
  input: {
23
+ startAdornment,
23
24
  endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { "data-testid": testid, disabled: value === '', onClick: () => {
24
25
  commit();
25
26
  }, children: _jsx(AddIcon, {}) }) })),
@@ -3,7 +3,6 @@ declare const BooleanEditor: ({ slot, }: {
3
3
  slot: {
4
4
  name: string;
5
5
  value: boolean | undefined;
6
- promotedBase?: unknown;
7
6
  set: (arg: boolean) => void;
8
7
  description: string;
9
8
  };
@@ -4,20 +4,7 @@ import { FormControl, FormHelperText } from '@mui/material';
4
4
  import { observer } from 'mobx-react';
5
5
  /** #slotEditor checkbox */
6
6
  const BooleanEditor = observer(function BooleanEditor({ slot, }) {
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 => {
7
+ return (_jsxs(FormControl, { children: [_jsx(LabeledCheckbox, { label: slot.name, checked: !!slot.value, onChange: val => {
21
8
  slot.set(val);
22
9
  } }), _jsx(FormHelperText, { children: slot.description })] }));
23
10
  });
@@ -1,9 +1,10 @@
1
1
  /** #slotEditor text field beside a swatch that opens a color picker */
2
- declare const ColorEditor: (props: {
2
+ declare const ColorEditor: ({ slot, }: {
3
3
  slot: {
4
4
  name: string;
5
5
  value: string | undefined;
6
6
  description: string;
7
+ contextVariable?: string[];
7
8
  set: (arg: string) => void;
8
9
  };
9
10
  }) => import("react").JSX.Element;
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense, lazy, useState } from 'react';
3
+ import { isCssColor } from '@jbrowse/core/util/colorBits';
4
+ import { isJexl } from '@jbrowse/core/util/jexlStrings';
3
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
6
  import { observer } from 'mobx-react';
5
7
  import ConfigurationTextField from './ConfigurationTextField.js';
@@ -17,18 +19,33 @@ const useStyles = makeStyles()({
17
19
  });
18
20
  // Also drives `maybeColor`, whose value is `undefined` while unset. An empty
19
21
  // 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.
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.
23
29
  /** #slotEditor text field beside a swatch that opens a color picker */
24
- const ColorEditor = observer(function ColorEditor(props) {
25
- const { slot } = props;
30
+ const ColorEditor = observer(function ColorEditor({ slot, }) {
31
+ const admitsJexl = !!slot.contextVariable?.length;
26
32
  const { classes } = useStyles();
33
+ const [draft, setDraft] = useState();
27
34
  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);
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);
30
46
  } }), _jsx(Suspense, { fallback: null, children: _jsx(PopoverPicker, { color: value, onChange: color => {
31
47
  slot.set(color);
48
+ setDraft(undefined);
32
49
  } }) })] }));
33
50
  });
34
51
  export default ColorEditor;
@@ -1,22 +1,22 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { getSlotDefinition, getTypeNamesFromExplicitlyTypedUnion, isConfigurationModel, isConfigurationSchemaType, isConfigurationSlot, makeSlotFacade, readConfObject, } from '@jbrowse/core/configuration';
3
+ import { arraySlotUnion, getSlotDefinition, getTypeNamesFromExplicitlyTypedUnion, isConfigurationModel, isConfigurationSchemaType, isConfigurationSlot, makeSlotFacade, readConfObject, setConf, } from '@jbrowse/core/configuration';
4
4
  import SanitizedHTML from '@jbrowse/core/ui/SanitizedHTML';
5
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
6
- 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';
7
9
  import ClearIcon from '@mui/icons-material/Clear';
10
+ import DeleteIcon from '@mui/icons-material/Delete';
8
11
  import ExpandLessIcon from '@mui/icons-material/ExpandLess';
9
12
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10
13
  import SearchIcon from '@mui/icons-material/Search';
11
- import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, InputAdornment, TextField, Typography, } from '@mui/material';
14
+ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, InputAdornment, MenuItem, TextField, Typography, } from '@mui/material';
12
15
  import { observer } from 'mobx-react';
13
16
  import SlotEditor from './SlotEditor.js';
14
17
  import TypeSelector from './TypeSelector.js';
15
18
  import { singularSlotName } from './singularSlotName.js';
16
19
  const useStyles = makeStyles()(theme => ({
17
- icon: {
18
- color: theme.palette.tertiary.contrastText || '#fff',
19
- },
20
20
  expansionPanelDetails: {
21
21
  display: 'block',
22
22
  padding: theme.spacing(1),
@@ -39,7 +39,15 @@ const useStyles = makeStyles()(theme => ({
39
39
  margin: theme.spacing(1, 0),
40
40
  textTransform: 'none',
41
41
  },
42
+ entryControls: {
43
+ display: 'flex',
44
+ justifyContent: 'flex-end',
45
+ },
42
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
+ }
43
51
  // matches a slot or sub-schema against a lowercased filter string, recursing
44
52
  // into sub-schemas so a nested slot name keeps its ancestors visible
45
53
  function memberMatches(schema, slotName, slotSchema, query) {
@@ -53,7 +61,7 @@ function memberMatches(schema, slotName, slotSchema, query) {
53
61
  return (slotName.toLowerCase().includes(query) ||
54
62
  (Array.isArray(slot)
55
63
  ? slot.some(subslot => schemaMatches(subslot, query))
56
- : schemaMatches(slot, query)));
64
+ : slot !== undefined && schemaMatches(slot, query)));
57
65
  }
58
66
  else {
59
67
  return false;
@@ -77,9 +85,32 @@ function displayDefaultExpanded(slot, expandedDisplayId) {
77
85
  : undefined;
78
86
  return !displayId || !expandedDisplayId || displayId === expandedDisplayId;
79
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
+ });
80
111
  const Member = observer(function Member(props) {
81
112
  const { classes } = useStyles();
82
- const { slotName, slotSchema, schema, slot = schema[slotName], path = [], filter = '', expandedDisplayId, } = props;
113
+ const { slotName, slotSchema, schema, slot = schema[slotName], path = [], filter = '', expandedDisplayId, entry, } = props;
83
114
  // when the sub-schema's own name matches, drop the filter for its children so
84
115
  // the whole group stays visible; otherwise keep filtering descendants
85
116
  const childFilter = slotName.toLowerCase().includes(filter.toLowerCase())
@@ -91,12 +122,7 @@ const Member = observer(function Member(props) {
91
122
  // keyed on Array.isArray, not `.length` truthiness, so an empty array maps
92
123
  // to nothing instead of being mis-handled as a single schema
93
124
  if (Array.isArray(slot)) {
94
- return slot.map((subslot, slotIndex) => {
95
- const key = subslot.type
96
- ? `${singularSlotName(slotName)} ${subslot.type}`
97
- : `${singularSlotName(slotName)} ${slotIndex + 1}`;
98
- return _jsx(Member, { ...props, slot: subslot, slotName: key }, key);
99
- });
125
+ return _jsx(EntryList, { ...props, entries: slot });
100
126
  }
101
127
  // if this is an explicitly typed schema, make a type-selecting dropdown
102
128
  // that can be used to change its type
@@ -107,13 +133,11 @@ const Member = observer(function Member(props) {
107
133
  // visible, not just present in the collapsed DOM. The key flips the
108
134
  // accordion between filtered/unfiltered mounts so the new
109
135
  // 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 => {
111
- if (evt.target.value !== slot.type) {
112
- schema.setSubschema(slotName, {
113
- type: evt.target.value,
114
- });
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 });
115
139
  }
116
- } })) : null, _jsx("div", { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName], filter: childFilter, expandedDisplayId: expandedDisplayId }) })] })] }, filter ? 'filtered' : 'unfiltered'));
140
+ } })) : null, slot ? (_jsx("div", { className: classes.noOverflow, children: _jsx(Schema, { schema: slot, path: [...path, slotName], filter: childFilter, expandedDisplayId: expandedDisplayId }) })) : null] })] }, filter ? 'filtered' : 'unfiltered'));
117
141
  }
118
142
  else if (isConfigurationSlot(schema, slotName)) {
119
143
  return _jsx(SlotEditor, { slot: makeSlotFacade(schema, slotName) }, slotName);
@@ -122,6 +146,30 @@ const Member = observer(function Member(props) {
122
146
  return null;
123
147
  }
124
148
  });
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
+ });
125
173
  const Schema = observer(function Schema({ schema, path = [], filter = '', expandedDisplayId, }) {
126
174
  const { classes } = useStyles();
127
175
  const [showAdvanced, setShowAdvanced] = useState(false);
@@ -147,7 +195,7 @@ const ConfigurationEditor = observer(function ConfigurationEditor({ model, }) {
147
195
  }
148
196
  const key = readConfObject(target, 'trackId');
149
197
  const name = readConfObject(target, 'name');
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 => {
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 => {
151
199
  setFilter(evt.target.value);
152
200
  }, size: "small", fullWidth: true, slotProps: {
153
201
  input: {
@@ -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,7 +4,6 @@ declare const JsonEditor: ({ slot, }: {
4
4
  name: string;
5
5
  description: string;
6
6
  value: unknown;
7
- promotedBase?: unknown;
8
7
  set: (arg: unknown) => void;
9
8
  };
10
9
  }) => import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { MonospaceTextField } from '@jbrowse/core/ui';
4
+ import { alpha } from '@jbrowse/core/ui/palette';
4
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
6
  import { observer } from 'mobx-react';
6
7
  const useStyles = makeStyles()(theme => ({
@@ -9,22 +10,17 @@ const useStyles = makeStyles()(theme => ({
9
10
  background: theme.palette.background.default,
10
11
  width: '100%',
11
12
  marginTop: '16px',
12
- border: '1px solid rgba(0,0,0,0.42)',
13
+ border: `1px solid ${alpha(theme.palette.text.primary, 0.42)}`,
13
14
  },
14
15
  }));
15
16
  /** #slotEditor monospace textarea holding arbitrary JSON */
16
17
  const JsonEditor = observer(function JsonEditor({ slot, }) {
17
18
  const { classes } = useStyles();
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
- });
19
+ // A `maybeFrozen` slot is `undefined` when unset, and `JSON.stringify(undefined)`
20
+ // returns the *value* `undefined` rather than a string — which handed
21
+ // MonospaceTextField an undefined `value` and made the field uncontrolled
22
+ // until the first keystroke (React then warns on the switch).
23
+ const [contents, setContents] = useState(() => slot.value === undefined ? '' : JSON.stringify(slot.value, null, 2));
28
24
  const [error, setError] = useState();
29
25
  return (_jsx(MonospaceTextField, { className: classes.jsonEditor, label: slot.name, value: contents, error: error, helperText: slot.description, onChange: val => {
30
26
  setContents(val);
@@ -4,7 +4,6 @@ declare const NumberEditor: ({ slot, integer, }: {
4
4
  name?: string;
5
5
  value: number | undefined;
6
6
  description?: string;
7
- promotedBase?: unknown;
8
7
  set: (val: number) => void;
9
8
  };
10
9
  integer?: boolean;
@@ -5,16 +5,7 @@ import ConfigurationTextField from './ConfigurationTextField.js';
5
5
  /** #slotEditor numeric text field */
6
6
  const NumberEditor = observer(function NumberEditor({ slot, integer = false, }) {
7
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 => {
8
+ return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: val, onChange: evt => {
18
9
  const v = evt.target.value;
19
10
  setVal(v);
20
11
  const num = Number(v);
@@ -10,16 +10,21 @@ import BooleanEditor from './BooleanEditor.js';
10
10
  import CallbackEditor from './CallbackEditor.js';
11
11
  import ColorEditor from './ColorEditor.js';
12
12
  import ConfigurationTextField from './ConfigurationTextField.js';
13
+ import { ColorArrayEditor, StringEnumArrayEditor } from './EntryListEditor.js';
13
14
  import IntegerEditor from './IntegerEditor.js';
14
15
  import JsonEditor from './JsonEditor.js';
15
16
  import NumberEditor from './NumberEditor.js';
16
17
  import NumberMapEditor from './NumberMapEditor.js';
17
- import StringArrayEditor from './StringArrayEditor.js';
18
+ import StringArrayEditor, { ExpressionArrayEditor, } from './StringArrayEditor.js';
18
19
  import StringArrayMapEditor from './StringArrayMapEditor.js';
20
+ import StringMapEditor from './StringMapEditor.js';
19
21
  import { useSlotEditorStyles } from './useSlotEditorStyles.js';
22
+ // Also drives `maybeString`, whose value is `undefined` while unset: clearing
23
+ // the field leaves the empty string, which is a value, and the reset button
24
+ // returns the slot to unset.
20
25
  /** #slotEditor single-line text field */
21
26
  const StringEditor = observer(function StringEditor({ slot, }) {
22
- return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: slot.value, onChange: evt => {
27
+ return (_jsx(ConfigurationTextField, { label: slot.name, helperText: slot.description, value: slot.value ?? '', onChange: evt => {
23
28
  slot.set(evt.target.value);
24
29
  } }));
25
30
  });
@@ -36,18 +41,10 @@ const UNSET_CHOICE = '';
36
41
  /** #slotEditor dropdown of the `model`'s members */
37
42
  const StringEnumEditor = observer(function StringEnumEditor({ slot, }) {
38
43
  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
44
  return (_jsxs(ConfigurationTextField, { value: slot.value ?? UNSET_CHOICE, label: slot.name, select: true, helperText: slot.description, onChange: evt => {
48
45
  const { value } = evt.target;
49
46
  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)))] }));
47
+ }, children: [nullable ? (_jsx(MenuItem, { value: UNSET_CHOICE, children: _jsx("em", { children: "default" }) })) : null, (slot.choices ?? []).map(str => (_jsx(MenuItem, { value: str, children: str }, str)))] }));
51
48
  });
52
49
  /** #slotEditor URL, local file path (desktop) or file blob (browser) */
53
50
  const FileSelectorWrapper = observer(function FileSelectorWrapper({ slot, }) {
@@ -62,11 +59,18 @@ const FileSelectorWrapper = observer(function FileSelectorWrapper({ slot, }) {
62
59
  // The editors stay fully typed internally; only this lookup is untyped.
63
60
  const valueComponents = {
64
61
  string: StringEditor,
62
+ maybeString: StringEditor,
63
+ featureField: StringEditor,
65
64
  text: TextEditor,
66
65
  fileLocation: FileSelectorWrapper,
66
+ maybeFileLocation: FileSelectorWrapper,
67
67
  stringArray: StringArrayEditor,
68
+ expressionArray: ExpressionArrayEditor,
69
+ colorArray: ColorArrayEditor,
70
+ stringEnumArray: StringEnumArrayEditor,
68
71
  stringArrayMap: StringArrayMapEditor,
69
72
  numberMap: NumberMapEditor,
73
+ stringMap: StringMapEditor,
70
74
  number: NumberEditor,
71
75
  maybeNumber: NumberEditor,
72
76
  integer: IntegerEditor,
@@ -85,8 +89,10 @@ const SlotEditor = observer(function SlotEditor({ slot, }) {
85
89
  // editor mode is UI-only state, derived once from whether the stored value is
86
90
  // a jexl callback. Toggling rewrites the value; typing a "jexl:" prefix into a
87
91
  // 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));
92
+ // the explicit toggle. A featureField's `jexl:` expression is a field, typed
93
+ // in its own text field.
94
+ const callback = slot.contextVariable.length > 0;
95
+ const [callbackMode, setCallbackMode] = useState(() => callback && isCallbackValue(slot.value));
90
96
  // bumped on reset to remount the value editor: the buffered editors (number,
91
97
  // json, callback) seed internal state from slot.value only on mount, so an
92
98
  // external set wouldn't otherwise show in the field
@@ -98,14 +104,12 @@ const SlotEditor = observer(function SlotEditor({ slot, }) {
98
104
  const ValueComponent = callbackMode
99
105
  ? CallbackEditor
100
106
  : (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
107
  const { modified } = slot;
104
108
  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
109
  slot.set(slot.defaultValue);
106
- setCallbackMode(isCallbackValue(slot.defaultValue));
110
+ setCallbackMode(callback && isCallbackValue(slot.defaultValue));
107
111
  setResetNonce(nonce => nonce + 1);
108
- }, children: _jsx(RestartAltIcon, {}) }) }) })) : null, slot.contextVariable.length ? (_jsx("div", { className: classes.slotModeSwitch, children: _jsx(Tooltip, { title: callbackMode
112
+ }, children: _jsx(RestartAltIcon, {}) }) }) })) : null, callback ? (_jsx("div", { className: classes.slotModeSwitch, children: _jsx(Tooltip, { title: callbackMode
109
113
  ? 'Editing as a callback (Jexl expression). Click to use a fixed value.'
110
114
  : 'Editing as a fixed value. Click to use a callback (Jexl expression).', children: _jsx(IconButton, { color: callbackMode ? 'primary' : 'default', onClick: () => {
111
115
  if (callbackMode) {
@@ -1,10 +1,17 @@
1
+ interface ArraySlot {
2
+ name: string;
3
+ value: string[];
4
+ set: (arg: string[]) => void;
5
+ description: string;
6
+ }
1
7
  /** #slotEditor "todolist" of text fields, one per entry, with add and delete */
2
- declare const StringArrayEditor: ({ slot, }: {
3
- slot: {
4
- name: string;
5
- value: string[];
6
- set: (arg: string[]) => void;
7
- description: string;
8
- };
8
+ declare const StringArrayEditor: ({ slot, prefix, }: {
9
+ slot: ArraySlot;
10
+ prefix?: string;
9
11
  }) => import("react").JSX.Element;
12
+ /** #slotEditor "todolist" of text fields, each after a fixed `jexl:` */
13
+ declare const ExpressionArrayEditor: ({ slot, }: {
14
+ slot: ArraySlot;
15
+ }) => import("react").JSX.Element;
16
+ export { ExpressionArrayEditor };
10
17
  export default StringArrayEditor;
@@ -1,21 +1,31 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { JEXL_PREFIX } from '@jbrowse/core/util/jexlStrings';
2
3
  import DeleteIcon from '@mui/icons-material/Delete';
3
4
  import { FormHelperText, IconButton, InputAdornment, InputLabel, List, ListItem, TextField, } from '@mui/material';
4
5
  import { observer } from 'mobx-react';
5
6
  import AddNewField from './AddNewField.js';
6
7
  /** #slotEditor "todolist" of text fields, one per entry, with add and delete */
7
- const StringArrayEditor = observer(function StringArrayEditor({ slot, }) {
8
+ const StringArrayEditor = observer(function StringArrayEditor({ slot, prefix = '', }) {
8
9
  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)));
10
+ const startAdornment = prefix ? (_jsx(InputAdornment, { position: "start", children: prefix })) : undefined;
11
+ 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.slice(prefix.length), onChange: evt => {
12
+ slot.set(value.map((v, i) => i === idx ? prefix + evt.target.value : v));
11
13
  }, slotProps: {
12
14
  input: {
15
+ startAdornment,
13
16
  endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
14
17
  slot.set(value.filter((_, i) => i !== idx));
15
18
  }, children: _jsx(DeleteIcon, {}) }) })),
16
19
  },
17
- } }) }, idx))), _jsx(ListItem, { disableGutters: true, children: _jsx(AddNewField, { testid: `stringArrayAdd-${slot.name}`, onAdd: val => {
18
- slot.set([...value, val]);
20
+ } }) }, idx))), _jsx(ListItem, { disableGutters: true, children: _jsx(AddNewField, { testid: `stringArrayAdd-${slot.name}`, startAdornment: startAdornment, onAdd: val => {
21
+ slot.set([...value, prefix + val]);
19
22
  } }) })] }), _jsx(FormHelperText, { children: slot.description })] }));
20
23
  });
24
+ // `jexl:` sits fixed in front of each entry, so no keystroke writes one the
25
+ // slot refuses
26
+ /** #slotEditor "todolist" of text fields, each after a fixed `jexl:` */
27
+ const ExpressionArrayEditor = observer(function ExpressionArrayEditor({ slot, }) {
28
+ return _jsx(StringArrayEditor, { slot: slot, prefix: JEXL_PREFIX });
29
+ });
30
+ export { ExpressionArrayEditor };
21
31
  export default StringArrayEditor;
@@ -0,0 +1,10 @@
1
+ /** #slotEditor one card per key, each holding that key's text field */
2
+ declare const StringMapEditor: ({ slot, }: {
3
+ slot: {
4
+ name: string;
5
+ value: Map<string, string>;
6
+ set: (val: Record<string, string>) => void;
7
+ description: string;
8
+ };
9
+ }) => import("react").JSX.Element;
10
+ export default StringMapEditor;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { TextField } from '@mui/material';
3
+ import { observer } from 'mobx-react';
4
+ import MapSlotEditor from './MapSlotEditor.js';
5
+ /** #slotEditor one card per key, each holding that key's text field */
6
+ const StringMapEditor = observer(function StringMapEditor({ slot, }) {
7
+ return (_jsx(MapSlotEditor, { name: slot.name, description: slot.description, entries: [...slot.value], emptyValue: "", setMap: val => {
8
+ slot.set(val);
9
+ }, renderValue: (val, set) => (_jsx(TextField, { fullWidth: true, value: val, onChange: evt => {
10
+ set(evt.target.value);
11
+ } })) }));
12
+ });
13
+ export default StringMapEditor;
@@ -1,7 +1,7 @@
1
1
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
2
  declare const TypeSelector: ({ typeNameChoices, slot, slotName, onChange, }: {
3
3
  typeNameChoices: string[];
4
- slot: AnyConfigurationModel;
4
+ slot: AnyConfigurationModel | undefined;
5
5
  slotName: string;
6
6
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
7
7
  }) => import("react").JSX.Element;
@@ -4,6 +4,6 @@ import { observer } from 'mobx-react';
4
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, variant: "outlined", 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 of ${slotName}`, select: true, variant: "outlined", fullWidth: true, onChange: onChange, children: typeNameChoices.map(str => (_jsx(MenuItem, { value: str, children: str }, str))) }) }) }));
8
8
  });
9
9
  export default TypeSelector;
@@ -1,10 +1,10 @@
1
- import { lazy } from 'react';
2
1
  import { ConfigurationSchema } from '@jbrowse/core/configuration';
3
2
  import { WidgetType } from '@jbrowse/core/pluggableElementTypes';
3
+ import { lazyWithPreload } from '@jbrowse/core/util/lazyWithPreload';
4
4
  import HeadingComponent from './components/HeadingComponent.js';
5
5
  import stateModelFactory from './model.js';
6
6
  const configSchema = ConfigurationSchema('ConfigurationEditorWidget', {});
7
- const LazyConfigurationEditorComponent = lazy(() => import('./components/ConfigurationEditor.js'));
7
+ const LazyConfigurationEditorComponent = lazyWithPreload(() => import('./components/ConfigurationEditor.js'));
8
8
  export default function registerConfigurationEditorWidget(pluginManager) {
9
9
  pluginManager.addWidgetType(() => {
10
10
  return new WidgetType({
@@ -16,25 +16,20 @@
16
16
  * ```
17
17
  */
18
18
  declare const configSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
19
- /**
20
- * #slot
21
- * stable identifier used as the adapter cache key; avoids hashing the
22
- * (potentially large) features array. optional — falls back to hash.
23
- */
19
+ type: {
20
+ type: "string";
21
+ } & {
22
+ defaultValue: string;
23
+ };
24
24
  readonly adapterId: {
25
- readonly type: "string";
26
- readonly defaultValue: "";
25
+ type: "string";
26
+ } & {
27
+ defaultValue: string;
27
28
  };
28
- /**
29
- * #slot
30
- * the features themselves, as an array of plain objects. Each needs a
31
- * `uniqueId`, `refName`, `start` and `end` (0-based half-open); anything
32
- * else — `name`, `type`, `score`, `strand`, nested `subfeatures` — is
33
- * carried through onto the feature as-is.
34
- */
35
29
  readonly features: {
36
- readonly type: "frozen";
37
- readonly defaultValue: readonly [];
30
+ type: "frozen";
31
+ } & {
32
+ defaultValue: unknown;
38
33
  };
39
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
34
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
40
35
  export default configSchema;
@@ -22,25 +22,20 @@
22
22
  * ```
23
23
  */
24
24
  declare const regionsConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
25
- /**
26
- * #slot
27
- * stable identifier used as the adapter cache key; avoids hashing the
28
- * (potentially large) features array. optional — falls back to hash.
29
- */
25
+ type: {
26
+ type: "string";
27
+ } & {
28
+ defaultValue: string;
29
+ };
30
30
  readonly adapterId: {
31
- readonly type: "string";
32
- readonly defaultValue: "";
31
+ type: "string";
32
+ } & {
33
+ defaultValue: string;
33
34
  };
34
- /**
35
- * #slot
36
- * one entry per reference sequence, each with a `uniqueId`, `refName`,
37
- * `start: 0` and `end` set to that sequence's length. This is what defines
38
- * the assembly's reference names and sizes; no bases are supplied, so
39
- * base-level views are empty.
40
- */
41
35
  readonly features: {
42
- readonly type: "frozen";
43
- readonly defaultValue: readonly [];
36
+ type: "frozen";
37
+ } & {
38
+ defaultValue: unknown;
44
39
  };
45
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
40
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
46
41
  export default regionsConfigSchema;
@@ -21,25 +21,20 @@
21
21
  * ```
22
22
  */
23
23
  declare const sequenceConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
24
- /**
25
- * #slot
26
- * stable identifier used as the adapter cache key; avoids hashing the
27
- * (potentially large) features array. optional — falls back to hash.
28
- */
24
+ type: {
25
+ type: "string";
26
+ } & {
27
+ defaultValue: string;
28
+ };
29
29
  readonly adapterId: {
30
- readonly type: "string";
31
- readonly defaultValue: "";
30
+ type: "string";
31
+ } & {
32
+ defaultValue: string;
32
33
  };
33
- /**
34
- * #slot
35
- * one entry per reference sequence, each with a `uniqueId`, `refName`,
36
- * `start`, `end`, and a `seq` string holding the bases for that span. The
37
- * bases live in the config, so this is for small sequences — a plasmid, a
38
- * test contig — not a genome.
39
- */
40
34
  readonly features: {
41
- readonly type: "frozen";
42
- readonly defaultValue: readonly [];
35
+ type: "frozen";
36
+ } & {
37
+ defaultValue: unknown;
43
38
  };
44
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
39
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
45
40
  export default sequenceConfigSchema;
@@ -1,6 +1,5 @@
1
- import { normalizeUriSnapshot } from '../normalizeUriSnapshot.ts';
2
1
  import type { Instance } from '@jbrowse/mobx-state-tree';
3
- export declare const normalizeSnapshot: typeof normalizeUriSnapshot;
2
+ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
4
3
  /**
5
4
  * #config NcbiSequenceReportAliasAdapter
6
5
  * can read "sequence_report.tsv" type files from NCBI
@@ -57,27 +56,19 @@ export declare const normalizeSnapshot: typeof normalizeUriSnapshot;
57
56
  * ```
58
57
  */
59
58
  declare const NcbiSequenceReportAliasAdapterConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
60
- /**
61
- * #slot
62
- * location of the `sequence_report.tsv` NCBI publishes with an assembly. It
63
- * carries the RefSeq, GenBank and UCSC-style name of every sequence, so one
64
- * file aliases them all without hand-writing a chromAlias table.
65
- */
59
+ type: {
60
+ type: "string";
61
+ } & {
62
+ defaultValue: string;
63
+ };
66
64
  readonly location: {
67
- readonly type: "fileLocation";
68
- readonly defaultValue: {
69
- readonly uri: "/path/to/my/sequence_report.tsv";
70
- readonly locationType: "UriLocation";
71
- };
65
+ type: "maybeFileLocation";
72
66
  };
73
- /**
74
- * #slot
75
- */
76
67
  readonly useNameOverride: {
77
- readonly type: "boolean";
78
- readonly defaultValue: true;
79
- readonly description: "forces usage of the UCSC names over the NCBI style names from a FASTA";
68
+ type: "boolean";
69
+ } & {
70
+ defaultValue: boolean;
80
71
  };
81
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
72
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
82
73
  export type NcbiSequenceReportAliasAdapterConfig = Instance<typeof NcbiSequenceReportAliasAdapterConfigSchema>;
83
74
  export default NcbiSequenceReportAliasAdapterConfigSchema;
@@ -1,6 +1,7 @@
1
- import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- import { normalizeUriSnapshot } from '../normalizeUriSnapshot.js';
3
- export const normalizeSnapshot = normalizeUriSnapshot;
1
+ import { ConfigurationSchema, expandUriShorthand, } from '@jbrowse/core/configuration';
2
+ export function normalizeSnapshot(snap) {
3
+ return expandUriShorthand(snap, 'location');
4
+ }
4
5
  /**
5
6
  * #config NcbiSequenceReportAliasAdapter
6
7
  * can read "sequence_report.tsv" type files from NCBI
@@ -64,11 +65,7 @@ const NcbiSequenceReportAliasAdapterConfigSchema = ConfigurationSchema('NcbiSequ
64
65
  * file aliases them all without hand-writing a chromAlias table.
65
66
  */
66
67
  location: {
67
- type: 'fileLocation',
68
- defaultValue: {
69
- uri: '/path/to/my/sequence_report.tsv',
70
- locationType: 'UriLocation',
71
- },
68
+ type: 'maybeFileLocation',
72
69
  },
73
70
  /**
74
71
  * #slot
@@ -1,6 +1,5 @@
1
- import { normalizeUriSnapshot } from '../normalizeUriSnapshot.ts';
2
1
  import type { Instance } from '@jbrowse/mobx-state-tree';
3
- export declare const normalizeSnapshot: typeof normalizeUriSnapshot;
2
+ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
4
3
  /**
5
4
  * #config RefNameAliasAdapter
6
5
  * can read "chromAliases" type files from UCSC or any tab separated file of
@@ -55,40 +54,24 @@ export declare const normalizeSnapshot: typeof normalizeUriSnapshot;
55
54
  * ```
56
55
  */
57
56
  declare const RefNameAliasAdapter: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
58
- /**
59
- * #slot
60
- * location of the alias table: a UCSC `chromAlias.txt`, or any
61
- * tab-separated file whose rows each list the alternate names of one
62
- * reference sequence (`chr1<TAB>1<TAB>NC_000001.11`). It is what lets a
63
- * `1`-named file load against a `chr1`-named assembly.
64
- */
57
+ type: {
58
+ type: "string";
59
+ } & {
60
+ defaultValue: string;
61
+ };
65
62
  readonly location: {
66
- readonly type: "fileLocation";
67
- readonly defaultValue: {
68
- readonly uri: "/path/to/my/aliases.txt";
69
- readonly locationType: "UriLocation";
70
- };
63
+ type: "maybeFileLocation";
71
64
  };
72
- /**
73
- * #slot
74
- * by default, the "ref names that match the fasta" are assumed to be in the
75
- * first column (0), change this variable if needed
76
- */
77
65
  readonly refNameColumn: {
78
- readonly type: "number";
79
- readonly defaultValue: 0;
80
- readonly advanced: true;
66
+ type: "number";
67
+ } & {
68
+ defaultValue: number;
81
69
  };
82
- /**
83
- * #slot
84
- * refNameColumnHeaderName
85
- */
86
70
  readonly refNameColumnHeaderName: {
87
- readonly type: "string";
88
- readonly description: "alternative to refNameColumn, instead looks at header (starts with # and finds column name)";
89
- readonly defaultValue: "";
90
- readonly advanced: true;
71
+ type: "string";
72
+ } & {
73
+ defaultValue: string;
91
74
  };
92
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
75
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
93
76
  export type RefNameAliasAdapterConfig = Instance<typeof RefNameAliasAdapter>;
94
77
  export default RefNameAliasAdapter;
@@ -1,6 +1,7 @@
1
- import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- import { normalizeUriSnapshot } from '../normalizeUriSnapshot.js';
3
- export const normalizeSnapshot = normalizeUriSnapshot;
1
+ import { ConfigurationSchema, expandUriShorthand, } from '@jbrowse/core/configuration';
2
+ export function normalizeSnapshot(snap) {
3
+ return expandUriShorthand(snap, 'location');
4
+ }
4
5
  /**
5
6
  * #config RefNameAliasAdapter
6
7
  * can read "chromAliases" type files from UCSC or any tab separated file of
@@ -59,15 +60,11 @@ const RefNameAliasAdapter = ConfigurationSchema('RefNameAliasAdapter', {
59
60
  * #slot
60
61
  * location of the alias table: a UCSC `chromAlias.txt`, or any
61
62
  * tab-separated file whose rows each list the alternate names of one
62
- * reference sequence (`chr1<TAB>1<TAB>NC_000001.11`). It is what lets a
63
+ * reference sequence (`chr1<TAB>1<TAB>NC_000001.11`). The table lets a
63
64
  * `1`-named file load against a `chr1`-named assembly.
64
65
  */
65
66
  location: {
66
- type: 'fileLocation',
67
- defaultValue: {
68
- uri: '/path/to/my/aliases.txt',
69
- locationType: 'UriLocation',
70
- },
67
+ type: 'maybeFileLocation',
71
68
  },
72
69
  /**
73
70
  * #slot
@@ -1,4 +1,4 @@
1
1
  import type PluginManager from '@jbrowse/core/PluginManager';
2
2
  import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
3
3
  import type { FileLocation } from '@jbrowse/core/util/types';
4
- export declare function readAliasRows(loc: FileLocation, pluginManager?: PluginManager, opts?: BaseOptions): Promise<string[][]>;
4
+ export declare function readAliasRows(loc: FileLocation | undefined, pluginManager?: PluginManager, opts?: BaseOptions): Promise<string[][]>;
package/esm/aliasUtils.js CHANGED
@@ -1,16 +1,9 @@
1
1
  import { fetchAndMaybeUnzipText } from '@jbrowse/core/util';
2
2
  import { openLocation } from '@jbrowse/core/util/io';
3
- import { isUriLocation } from '@jbrowse/core/util/types';
4
- // The alias adapters' `location` slots default to a "/path/to/my/..."
5
- // placeholder; that (and a blank uri) means "no file configured", so the
6
- // adapter yields no aliases rather than trying to fetch the placeholder path.
7
- function isUnconfiguredLocation(loc) {
8
- return isUriLocation(loc) && (!loc.uri || loc.uri.startsWith('/path/to/my/'));
9
- }
10
3
  // Reads a tab-separated alias file into rows of columns, dropping blank lines.
11
4
  // Returns [] when no file is configured.
12
5
  export async function readAliasRows(loc, pluginManager, opts) {
13
- if (isUnconfiguredLocation(loc)) {
6
+ if (!loc) {
14
7
  return [];
15
8
  }
16
9
  // fetchAndMaybeUnzipText rather than readFile('utf8') so the read reports byte
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-config",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 config utilities",
6
6
  "keywords": [
@@ -21,13 +21,13 @@
21
21
  "esm"
22
22
  ],
23
23
  "dependencies": {
24
- "@jbrowse/mobx-state-tree": "^6.5.1",
25
- "@mui/icons-material": "^9.3.1",
26
- "@mui/material": "^9.3.1",
27
- "mobx": "^7.0.3",
24
+ "@jbrowse/mobx-state-tree": "^6.6.1",
25
+ "@mui/icons-material": "^9.4.0",
26
+ "@mui/material": "^9.4.0",
27
+ "mobx": "^7.0.5",
28
28
  "mobx-react": "^10.0.2",
29
29
  "rxjs": "^7.8.2",
30
- "@jbrowse/core": "^5.0.0-beta.1"
30
+ "@jbrowse/core": "^5.0.0-beta.10"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=19.0.0",
@@ -1 +0,0 @@
1
- export declare function normalizeUriSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
@@ -1,13 +0,0 @@
1
- // allows a minimal config of `{ uri: "..." }` by lifting `uri`/`baseUri` into
2
- // a proper `location` fileLocation slot. Shared by the alias adapters.
3
- export function normalizeUriSnapshot(snap) {
4
- return snap.uri
5
- ? {
6
- ...snap,
7
- location: {
8
- uri: snap.uri,
9
- baseUri: snap.baseUri,
10
- },
11
- }
12
- : snap;
13
- }