@jbrowse/plugin-config 5.0.0-beta.10 → 5.0.0-beta.13

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.
@@ -71,8 +71,8 @@ function schemaMatches(schema, query) {
71
71
  const { properties } = getMembers(schema);
72
72
  return Object.entries(properties).some(([slotName, slotSchema]) => memberMatches(schema, slotName, slotSchema, query));
73
73
  }
74
- // a slot flagged `advanced` in its schema definition, hidden behind a toggle
75
- function isAdvancedSlot(schema, slotName) {
74
+ // narrower than the About dialog's Advanced card, which also takes callbacks
75
+ function hidesBehindAdvancedToggle(schema, slotName) {
76
76
  return (isConfigurationSlot(schema, slotName) &&
77
77
  !!getSlotDefinition(schema, slotName).advanced);
78
78
  }
@@ -176,8 +176,8 @@ const Schema = observer(function Schema({ schema, path = [], filter = '', expand
176
176
  const query = filter.toLowerCase();
177
177
  const properties = getMembers(schema).properties;
178
178
  const visible = Object.entries(properties).filter(([slotName, slotSchema]) => !query || memberMatches(schema, slotName, slotSchema, query));
179
- const normal = visible.filter(([slotName]) => !isAdvancedSlot(schema, slotName));
180
- const advanced = visible.filter(([slotName]) => isAdvancedSlot(schema, slotName));
179
+ const normal = visible.filter(([slotName]) => !hidesBehindAdvancedToggle(schema, slotName));
180
+ const advanced = visible.filter(([slotName]) => hidesBehindAdvancedToggle(schema, slotName));
181
181
  const renderMember = ([slotName, slotSchema]) => (_jsx(Member, { slotName: slotName, slotSchema: slotSchema, path: path, schema: schema, filter: filter, expandedDisplayId: expandedDisplayId }, slotName));
182
182
  return (_jsxs(_Fragment, { children: [normal.map(renderMember), advanced.length && !query ? (_jsxs(Button, { className: classes.advancedToggle, size: "small", startIcon: showAdvanced ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: () => {
183
183
  setShowAdvanced(!showAdvanced);
@@ -52,11 +52,9 @@ const FileSelectorWrapper = observer(function FileSelectorWrapper({ slot, }) {
52
52
  slot.set(location);
53
53
  }, name: slot.name, description: slot.description, rootModel: slot.pluginManager.rootModel }));
54
54
  });
55
- // dynamic dispatch from a runtime slot `type` string to a typed editor. The
56
- // `any` is irreducible here: each editor declares a narrow `slot.value` type
57
- // (number, boolean, FileLocation, string[], ...) but makeSlotFacade can only
58
- // type value as `unknown`, so no single registry prop type satisfies them all.
59
- // The editors stay fully typed internally; only this lookup is untyped.
55
+ // The `any` is irreducible: each editor declares a narrow `slot.value` type, but
56
+ // makeSlotFacade can only type value as `unknown`. The keys are checked: a slot
57
+ // type with no editor is a compile error.
60
58
  const valueComponents = {
61
59
  string: StringEditor,
62
60
  maybeString: StringEditor,
@@ -97,13 +95,9 @@ const SlotEditor = observer(function SlotEditor({ slot, }) {
97
95
  // json, callback) seed internal state from slot.value only on mount, so an
98
96
  // external set wouldn't otherwise show in the field
99
97
  const [resetNonce, setResetNonce] = useState(0);
100
- const TypedComponent = valueComponents[type];
101
- if (!callbackMode && !TypedComponent) {
102
- console.warn(`no slot editor defined for ${type}, editing as string`);
103
- }
104
98
  const ValueComponent = callbackMode
105
99
  ? CallbackEditor
106
- : (TypedComponent ?? StringEditor);
100
+ : valueComponents[type];
107
101
  const { modified } = slot;
108
102
  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: () => {
109
103
  slot.set(slot.defaultValue);
@@ -9,6 +9,7 @@ export default function registerConfigurationEditorWidget(pluginManager) {
9
9
  pluginManager.addWidgetType(() => {
10
10
  return new WidgetType({
11
11
  name: 'ConfigurationEditorWidget',
12
+ discardOnClose: true,
12
13
  HeadingComponent,
13
14
  configSchema,
14
15
  stateModel: stateModelFactory(pluginManager),
@@ -8,11 +8,34 @@ import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
8
8
  export default function stateModelFactory(_pluginManager: PluginManager): import("@jbrowse/mobx-state-tree").IModelType<{
9
9
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
10
10
  type: import("@jbrowse/mobx-state-tree").ILiteralType<"ConfigurationEditorWidget">;
11
+ /**
12
+ * #property
13
+ * the track whose working copy this edits, re-resolved on every read so
14
+ * an undo that replaces the copy moves the editor with it
15
+ */
16
+ trackId: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
11
17
  }, {
12
- target: AnyConfigurationModel | undefined;
18
+ inlineTarget: AnyConfigurationModel | undefined;
13
19
  expandedDisplayId: string | undefined;
14
20
  } & {
21
+ /**
22
+ * #getter
23
+ */
24
+ readonly target: AnyConfigurationModel | undefined;
25
+ } & {
26
+ /**
27
+ * #action
28
+ * edit the working copy of `trackId`, saving any edit still pending
29
+ * on the previous target first
30
+ */
31
+ setTrackId(trackId: string): void;
32
+ /**
33
+ * #action
34
+ * edit a config node directly, saving any edit still pending on the
35
+ * previous target first
36
+ */
15
37
  setTarget(newTarget: AnyConfigurationModel | undefined): void;
16
38
  setExpandedDisplayId(displayId: string | undefined): void;
17
39
  afterCreate(): void;
40
+ beforeDetach(): void;
18
41
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
@@ -1,4 +1,5 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
+ import { getEditableTrackConfigById } from '@jbrowse/core/util/types';
2
3
  import { ElementId } from '@jbrowse/core/util/types/mst';
3
4
  import { addDisposer, getSnapshot, types } from '@jbrowse/mobx-state-tree';
4
5
  import { autorun } from 'mobx';
@@ -12,60 +13,92 @@ export default function stateModelFactory(_pluginManager) {
12
13
  .model('ConfigurationEditorWidget', {
13
14
  id: ElementId,
14
15
  type: types.literal('ConfigurationEditorWidget'),
16
+ /**
17
+ * #property
18
+ * the track whose working copy this edits, re-resolved on every read so
19
+ * an undo that replaces the copy moves the editor with it
20
+ */
21
+ trackId: types.maybe(types.string),
15
22
  })
16
23
  .volatile(() => ({
17
- // Target is stored as volatile since it doesn't need to be serialized.
18
- // The target is an MST model from track.configuration (which creates
19
- // an MST model from frozen config via ConfigurationReference).
20
- target: undefined,
24
+ // a config the session cannot resolve by trackId: an assembly, a
25
+ // connection, or a track a view holds inline
26
+ inlineTarget: undefined,
21
27
  // displayId of the display active in the view this editor was opened
22
28
  // from; its config accordion expands by default while the track's other
23
29
  // (incompatible/inactive) displays start collapsed
24
30
  expandedDisplayId: undefined,
25
31
  }))
26
- .actions(self => ({
27
- setTarget(newTarget) {
28
- self.target = newTarget;
32
+ .views(self => ({
33
+ /**
34
+ * #getter
35
+ */
36
+ get target() {
37
+ return self.trackId === undefined
38
+ ? self.inlineTarget
39
+ : getEditableTrackConfigById(getSession(self), self.trackId);
29
40
  },
30
- setExpandedDisplayId(displayId) {
31
- self.expandedDisplayId = displayId;
32
- },
33
- afterCreate() {
34
- let timeout;
35
- // Auto-save configuration changes with 400ms debounce. The autorun
36
- // reacts to any changes in the target configuration model and persists
37
- // them back to the session after a short delay via
38
- // updateTrackConfiguration, which routes admin edits to the jbrowse
39
- // config in place and everyone else's to a shareable per-track delta
40
- // (trackConfigDeltas) against the admin base. It keys off trackId; in
41
- // practice the widget is only opened on track configs. A config with no
42
- // trackId (assembly/connection) degrades gracefully: updateTrackConfiguration
43
- // finds no base/session/connection home and the edit just applies to the
44
- // live MST node in memory for this session.
45
- //
46
- // BaseTrackModel's afterAttach runs a sibling debounced save (a reaction
47
- // on the same config node) for direct setSlot quick-edits on a *shown*
48
- // track. Both intentionally coexist: this widget also handles an unshown
49
- // track edited from the selector, which has no BaseTrackModel. When both
50
- // fire they compute an identical delta, deduped in updateTrackConfiguration
51
- // — don't drop one to "simplify".
52
- addDisposer(self, autorun(() => {
53
- if (self.target) {
54
- // track configs (the practical case) carry a trackId; a
55
- // trackId-less snapshot degrades to an in-memory edit (see above)
56
- const snapshot = getSnapshot(self.target);
41
+ }))
42
+ .actions(self => {
43
+ let timeout;
44
+ let unsaved;
45
+ function save() {
46
+ clearTimeout(timeout);
47
+ if (unsaved) {
48
+ const session = getSession(self);
49
+ session.updateTrackConfiguration(unsaved);
50
+ unsaved = undefined;
51
+ }
52
+ }
53
+ return {
54
+ /**
55
+ * #action
56
+ * edit the working copy of `trackId`, saving any edit still pending
57
+ * on the previous target first
58
+ */
59
+ setTrackId(trackId) {
60
+ save();
61
+ self.inlineTarget = undefined;
62
+ self.trackId = trackId;
63
+ },
64
+ /**
65
+ * #action
66
+ * edit a config node directly, saving any edit still pending on the
67
+ * previous target first
68
+ */
69
+ setTarget(newTarget) {
70
+ save();
71
+ self.trackId = undefined;
72
+ self.inlineTarget = newTarget;
73
+ },
74
+ setExpandedDisplayId(displayId) {
75
+ self.expandedDisplayId = displayId;
76
+ },
77
+ afterCreate() {
78
+ // Saves through updateTrackConfiguration as a per-track delta. A
79
+ // config with no trackId (assembly/connection) finds no home there,
80
+ // so the edit stays on the live MST node for this session.
81
+ //
82
+ // BaseTrackModel runs a sibling saver on a shown track's working
83
+ // copy, which this widget edits too; this one covers a track that is
84
+ // not shown. When both fire they compute an identical delta, deduped
85
+ // in updateTrackConfiguration.
86
+ addDisposer(self, autorun(() => {
87
+ if (self.target) {
88
+ unsaved = getSnapshot(self.target);
89
+ clearTimeout(timeout);
90
+ timeout = setTimeout(save, 400);
91
+ }
92
+ }));
93
+ addDisposer(self, () => {
57
94
  clearTimeout(timeout);
58
- timeout = setTimeout(() => {
59
- const session = getSession(self);
60
- session.updateTrackConfiguration(snapshot);
61
- }, 400);
62
- }
63
- }));
64
- // ensure a pending debounced save can't fire after disposal, where
65
- // getSession(self) would throw on the now-detached node
66
- addDisposer(self, () => {
67
- clearTimeout(timeout);
68
- });
69
- },
70
- }));
95
+ });
96
+ },
97
+ // closing the editor detaches it from the session, so an edit still
98
+ // inside the debounce saves now, while getSession(self) resolves
99
+ beforeDetach() {
100
+ save();
101
+ },
102
+ };
103
+ });
71
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-config",
3
- "version": "5.0.0-beta.10",
3
+ "version": "5.0.0-beta.13",
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.6.1",
24
+ "@jbrowse/mobx-state-tree": "^6.7.0",
25
25
  "@mui/icons-material": "^9.4.0",
26
26
  "@mui/material": "^9.4.0",
27
- "mobx": "^7.0.5",
27
+ "mobx": "^7.0.6",
28
28
  "mobx-react": "^10.0.2",
29
29
  "rxjs": "^7.8.2",
30
- "@jbrowse/core": "^5.0.0-beta.10"
30
+ "@jbrowse/core": "^5.0.0-beta.13"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=19.0.0",